/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */

#include "common.h"
#include <glib/gstdio.h>

#ifdef WIN32
#include <windows.h>
#include <shlobj.h>
#endif

#include <pthread.h>

#include "utils.h"
#define DEBUG_FLAG SEAFILE_DEBUG_SYNC
#include "log.h"

#include "vc-utils.h"

#include "seafile-session.h"
#include "seafile-config.h"
#include "commit-mgr.h"
#include "branch-mgr.h"
#include "repo-mgr.h"
#include "fs-mgr.h"
#include "seafile-error.h"
#include "seafile-crypt.h"
#include "index/index.h"
#include "index/cache-tree.h"
#include "diff-simple.h"
#include "change-set.h"

#include "db.h"

#include "seafile-object.h"

#define INDEX_DIR "index"
#define IGNORE_FILE "seafile-ignore.txt"

#ifdef HAVE_KEYSTORAGE_GK
#include "repokey/seafile-gnome-keyring.h"
#endif // HAVE_KEYSTORAGE_GK

#ifndef SEAFILE_CLIENT_VERSION
#define SEAFILE_CLIENT_VERSION PACKAGE_VERSION
#endif

struct _SeafRepoManagerPriv {
    GHashTable *repo_hash;
    sqlite3    *db;
    pthread_mutex_t db_lock;
    GHashTable *checkout_tasks_hash;
    pthread_rwlock_t lock;

    GHashTable *user_perms;     /* repo_id -> folder user perms */
    GHashTable *group_perms;    /* repo_id -> folder group perms */
    pthread_mutex_t perm_lock;

    GAsyncQueue *lock_office_job_queue;

    // sync_errors is used to record sync errors for which notifications have been sent to avoid repeated notifications of the same error.
    GList *sync_errors;
    pthread_mutex_t errors_lock;

    GHashTable* block_map_cache_table;
    pthread_rwlock_t block_map_lock;
};

static const char *ignore_table[] = {
    /* tmp files under Linux */
    "*~",
    /* Seafile's backup file */
    "*.sbak",
    /* Emacs tmp files */
    "#*#",
    /* windows image cache */
    "Thumbs.db",
    /* For Mac */
    ".DS_Store",
    "._*",
    NULL,
};

#define CONFLICT_PATTERN " \\(SFConflict .+\\)"

#define OFFICE_LOCK_PATTERN "~\\$(.+)$"
#define LIBRE_OFFICE_LOCK_PATTERN "\\.~lock\\.(.+)#$"
#define WPS_LOCK_PATTERN "\\.~(.+)$"

static GPatternSpec** ignore_patterns;
static GPatternSpec* office_temp_ignore_patterns[5];
static GRegex *conflict_pattern = NULL;
static GRegex *office_lock_pattern = NULL;
static GRegex *libre_office_lock_pattern = NULL;
static GRegex *wps_lock_pattern = NULL;

static SeafRepo *
load_repo (SeafRepoManager *manager, const char *repo_id);

static void load_repos (SeafRepoManager *manager, const char *seaf_dir);
static void seaf_repo_manager_del_repo_property (SeafRepoManager *manager,
                                                 const char *repo_id);

static int save_branch_repo_map (SeafRepoManager *manager, SeafBranch *branch);
static void save_repo_property (SeafRepoManager *manager,
                                const char *repo_id,
                                const char *key, const char *value);

static void
locked_file_free (LockedFile *file)
{
    if (!file)
        return;
    g_free (file->operation);
    g_free (file);
}

static gboolean
load_locked_file (sqlite3_stmt *stmt, void *data)
{
    GHashTable *ret = data;
    LockedFile *file;
    const char *path, *operation, *file_id;
    gint64 old_mtime;

    path = (const char *)sqlite3_column_text (stmt, 0);
    operation = (const char *)sqlite3_column_text (stmt, 1);
    old_mtime = sqlite3_column_int64 (stmt, 2);
    file_id = (const char *)sqlite3_column_text (stmt, 3);

    file = g_new0 (LockedFile, 1);
    file->operation = g_strdup(operation);
    file->old_mtime = old_mtime;
    if (file_id)
        memcpy (file->file_id, file_id, 40);

    g_hash_table_insert (ret, g_strdup(path), file);

    return TRUE;
}

LockedFileSet *
seaf_repo_manager_get_locked_file_set (SeafRepoManager *mgr, const char *repo_id)
{
    GHashTable *locked_files = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                      g_free,
                                                      (GDestroyNotify)locked_file_free);
    char sql[256];

    sqlite3_snprintf (sizeof(sql), sql,
                      "SELECT path, operation, old_mtime, file_id FROM LockedFiles "
                      "WHERE repo_id = '%q'",
                      repo_id);

    pthread_mutex_lock (&mgr->priv->db_lock);

    /* Ingore database error. We return an empty set on error. */
    sqlite_foreach_selected_row (mgr->priv->db, sql,
                                 load_locked_file, locked_files);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    LockedFileSet *ret = g_new0 (LockedFileSet, 1);
    ret->mgr = mgr;
    memcpy (ret->repo_id, repo_id, 36);
    ret->locked_files = locked_files;

    return ret;
}

void
locked_file_set_free (LockedFileSet *fset)
{
    if (!fset)
        return;
    g_hash_table_destroy (fset->locked_files);
    g_free (fset);
}

int
locked_file_set_add_update (LockedFileSet *fset,
                            const char *path,
                            const char *operation,
                            gint64 old_mtime,
                            const char *file_id)
{
    SeafRepoManager *mgr = fset->mgr;
    char *sql;
    sqlite3_stmt *stmt;
    LockedFile *file;
    gboolean exists;

    exists = (g_hash_table_lookup (fset->locked_files, path) != NULL);

    pthread_mutex_lock (&mgr->priv->db_lock);

    if (!exists) {
        seaf_debug ("New locked file record %.8s, %s, %s, %"
                    G_GINT64_FORMAT".\n",
                    fset->repo_id, path, operation, old_mtime);

        sql = "INSERT INTO LockedFiles VALUES (?, ?, ?, ?, ?, NULL)";
        stmt = sqlite_query_prepare (mgr->priv->db, sql);
        sqlite3_bind_text (stmt, 1, fset->repo_id, -1, SQLITE_TRANSIENT);
        sqlite3_bind_text (stmt, 2, path, -1, SQLITE_TRANSIENT);
        sqlite3_bind_text (stmt, 3, operation, -1, SQLITE_TRANSIENT);
        sqlite3_bind_int64 (stmt, 4, old_mtime);
        sqlite3_bind_text (stmt, 5, file_id, -1, SQLITE_TRANSIENT);
        if (sqlite3_step (stmt) != SQLITE_DONE) {
            seaf_warning ("Failed to insert locked file %s to db: %s.\n",
                          path, sqlite3_errmsg (mgr->priv->db));
            sqlite3_finalize (stmt);
            pthread_mutex_unlock (&mgr->priv->db_lock);
            return -1;
        }
        sqlite3_finalize (stmt);

        file = g_new0 (LockedFile, 1);
        file->operation = g_strdup(operation);
        file->old_mtime = old_mtime;
        if (file_id)
            memcpy (file->file_id, file_id, 40);

        g_hash_table_insert (fset->locked_files, g_strdup(path), file);
    } else {
        seaf_debug ("Update locked file record %.8s, %s, %s.\n",
                    fset->repo_id, path, operation);

        /* If a UPDATE record exists, don't update the old_mtime.
         * We need to keep the old mtime when the locked file was first detected.
         */

        sql = "UPDATE LockedFiles SET operation = ?, file_id = ? "
            "WHERE repo_id = ? AND path = ?";
        stmt = sqlite_query_prepare (mgr->priv->db, sql);
        sqlite3_bind_text (stmt, 1, operation, -1, SQLITE_TRANSIENT);
        sqlite3_bind_text (stmt, 2, file_id, -1, SQLITE_TRANSIENT);
        sqlite3_bind_text (stmt, 3, fset->repo_id, -1, SQLITE_TRANSIENT);
        sqlite3_bind_text (stmt, 4, path, -1, SQLITE_TRANSIENT);
        if (sqlite3_step (stmt) != SQLITE_DONE) {
            seaf_warning ("Failed to update locked file %s to db: %s.\n",
                          path, sqlite3_errmsg (mgr->priv->db));
            sqlite3_finalize (stmt);
            pthread_mutex_unlock (&mgr->priv->db_lock);
            return -1;
        }
        sqlite3_finalize (stmt);

        file = g_hash_table_lookup (fset->locked_files, path);
        g_free (file->operation);
        file->operation = g_strdup(operation);
        if (file_id)
            memcpy (file->file_id, file_id, 40);
    }

    pthread_mutex_unlock (&mgr->priv->db_lock);

    return 0;
}

int
locked_file_set_remove (LockedFileSet *fset, const char *path, gboolean db_only)
{
    SeafRepoManager *mgr = fset->mgr;
    char *sql;
    sqlite3_stmt *stmt;

    if (g_hash_table_lookup (fset->locked_files, path) == NULL)
        return 0;

    seaf_debug ("Remove locked file record %.8s, %s.\n",
                fset->repo_id, path);

    pthread_mutex_lock (&mgr->priv->db_lock);

    sql = "DELETE FROM LockedFiles WHERE repo_id = ? AND path = ?";
    stmt = sqlite_query_prepare (mgr->priv->db, sql);
    sqlite3_bind_text (stmt, 1, fset->repo_id, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text (stmt, 2, path, -1, SQLITE_TRANSIENT);
    if (sqlite3_step (stmt) != SQLITE_DONE) {
        seaf_warning ("Failed to remove locked file %s from db: %s.\n",
                      path, sqlite3_errmsg (mgr->priv->db));
        sqlite3_finalize (stmt);
        pthread_mutex_unlock (&mgr->priv->db_lock);
        return -1;
    }
    sqlite3_finalize (stmt);
    pthread_mutex_unlock (&mgr->priv->db_lock);

    if (!db_only)
        g_hash_table_remove (fset->locked_files, path);

    return 0;
}

LockedFile *
locked_file_set_lookup (LockedFileSet *fset, const char *path)
{
    return (LockedFile *) g_hash_table_lookup (fset->locked_files, path);
}

/* Folder permissions. */

FolderPerm *
folder_perm_new (const char *path, const char *permission)
{
    FolderPerm *perm = g_new0 (FolderPerm, 1);

    perm->path = g_strdup(path);
    perm->permission = g_strdup(permission);

    return perm;
}

void
folder_perm_free (FolderPerm *perm)
{
    if (!perm)
        return;

    g_free (perm->path);
    g_free (perm->permission);
    g_free (perm);
}

static GList *
folder_perm_list_copy (GList *perms)
{
    GList *ret = NULL, *ptr;
    FolderPerm *perm, *new_perm;

    for (ptr = perms; ptr; ptr = ptr->next) {
        perm = ptr->data;
        new_perm = folder_perm_new (perm->path, perm->permission);
        ret = g_list_append (ret, new_perm);
    }

    return ret;
}

static gint
comp_folder_perms (gconstpointer a, gconstpointer b)
{
    const FolderPerm *perm_a = a, *perm_b = b;

    return (strcmp (perm_b->path, perm_a->path));
}

static void
delete_folder_perm (SeafRepoManager *mgr, const char *repo_id, FolderPermType type, FolderPerm *perm)
{
    GList *folder_perms = NULL;
    if (type == FOLDER_PERM_TYPE_USER) {
        folder_perms = g_hash_table_lookup (mgr->priv->user_perms, repo_id);
        if (!folder_perms)
            return;

        if (g_strcmp0 (perm->path, "") == 0) {
            g_list_free_full (folder_perms, (GDestroyNotify)folder_perm_free);
            g_hash_table_remove (mgr->priv->user_perms, repo_id);
            return;
        }

        GList *existing = g_list_find_custom (folder_perms,
                                              perm,
                                              comp_folder_perms);
        if (existing) {
            FolderPerm *old_perm = existing->data;
            folder_perms = g_list_remove (folder_perms, old_perm);
            folder_perm_free (old_perm);
            g_hash_table_insert (mgr->priv->user_perms, g_strdup(repo_id), folder_perms);
        }
    } else if (type == FOLDER_PERM_TYPE_GROUP) {
        folder_perms = g_hash_table_lookup (mgr->priv->group_perms, repo_id);
        if (!folder_perms)
            return;

        if (g_strcmp0 (perm->path, "") == 0) {
            g_list_free_full (folder_perms, (GDestroyNotify)folder_perm_free);
            g_hash_table_remove (mgr->priv->group_perms, repo_id);
            return;
        }

        GList *existing = g_list_find_custom (folder_perms,
                                              perm,
                                              comp_folder_perms);
        if (existing) {
            FolderPerm *old_perm = existing->data;
            folder_perms = g_list_remove (folder_perms, old_perm);
            folder_perm_free (old_perm);
            g_hash_table_insert (mgr->priv->group_perms, g_strdup(repo_id), folder_perms);
        }
    }
}

int
seaf_repo_manager_delete_folder_perm (SeafRepoManager *mgr,
                                      const char *repo_id,
                                      FolderPermType type,
                                      FolderPerm *perm)
{
    char *sql;
    sqlite3_stmt *stmt;

    g_return_val_if_fail ((type == FOLDER_PERM_TYPE_USER ||
                           type == FOLDER_PERM_TYPE_GROUP),
                          -1);

    if (!perm) {
        return 0;
    }

    /* Update db. */
    pthread_mutex_lock (&mgr->priv->db_lock);

    if (g_strcmp0 (perm->path, "") == 0) {
        if (type == FOLDER_PERM_TYPE_USER)
            sql = "DELETE FROM FolderUserPerms WHERE repo_id = ?";
        else
            sql = "DELETE FROM FolderGroupPerms WHERE repo_id = ?";
    } else {
        if (type == FOLDER_PERM_TYPE_USER)
            sql = "DELETE FROM FolderUserPerms WHERE repo_id = ? and path = ?";
        else
            sql = "DELETE FROM FolderGroupPerms WHERE repo_id = ? and path = ?";
    }

    stmt = sqlite_query_prepare (mgr->priv->db, sql);
    sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT);
    if (g_strcmp0 (perm->path, "") != 0)
        sqlite3_bind_text (stmt, 2, perm->path, -1, SQLITE_TRANSIENT);
    if (sqlite3_step (stmt) != SQLITE_DONE) {
        seaf_warning ("Failed to remove folder perm for %.8s: %s.\n",
                      repo_id, sqlite3_errmsg (mgr->priv->db));
        sqlite3_finalize (stmt);
        pthread_mutex_unlock (&mgr->priv->db_lock);
        return -1;
    }
    sqlite3_finalize (stmt);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    /* Update in memory */
    pthread_mutex_lock (&mgr->priv->perm_lock);
    delete_folder_perm (mgr, repo_id, type, perm);
    pthread_mutex_unlock (&mgr->priv->perm_lock);

    return 0;
}

int
seaf_repo_manager_update_folder_perm (SeafRepoManager *mgr,
                                      const char *repo_id,
                                      FolderPermType type,
                                      FolderPerm *perm)
{
    char *sql;
    sqlite3_stmt *stmt;

    g_return_val_if_fail ((type == FOLDER_PERM_TYPE_USER ||
                           type == FOLDER_PERM_TYPE_GROUP),
                          -1);

    if (!perm) {
        return 0;
    }

    /* Update db. */
    pthread_mutex_lock (&mgr->priv->db_lock);

    if (type == FOLDER_PERM_TYPE_USER)
        sql = "DELETE FROM FolderUserPerms WHERE repo_id = ? and path = ?";
    else
        sql = "DELETE FROM FolderGroupPerms WHERE repo_id = ? and path = ?";
    stmt = sqlite_query_prepare (mgr->priv->db, sql);
    sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text (stmt, 2, perm->path, -1, SQLITE_TRANSIENT);
    if (sqlite3_step (stmt) != SQLITE_DONE) {
        seaf_warning ("Failed to remove folder perm for %.8s(%s): %s.\n",
                      repo_id, perm->path, sqlite3_errmsg (mgr->priv->db));
        sqlite3_finalize (stmt);
        pthread_mutex_unlock (&mgr->priv->db_lock);
        return -1;
    }
    sqlite3_finalize (stmt);

    if (type == FOLDER_PERM_TYPE_USER)
        sql = "INSERT INTO FolderUserPerms VALUES (?, ?, ?)";
    else
        sql = "INSERT INTO FolderGroupPerms VALUES (?, ?, ?)";
    stmt = sqlite_query_prepare (mgr->priv->db, sql);

    sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text (stmt, 2, perm->path, -1, SQLITE_TRANSIENT);
    sqlite3_bind_text (stmt, 3, perm->permission, -1, SQLITE_TRANSIENT);

    if (sqlite3_step (stmt) != SQLITE_DONE) {
        seaf_warning ("Failed to insert folder perm for %.8s(%s): %s.\n",
                      repo_id, perm->path, sqlite3_errmsg (mgr->priv->db));
        sqlite3_finalize (stmt);
        pthread_mutex_unlock (&mgr->priv->db_lock);
        return -1;
    }

    sqlite3_finalize (stmt);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    /* Update in memory */
    GList *folder_perms;

    pthread_mutex_lock (&mgr->priv->perm_lock);
    if (type == FOLDER_PERM_TYPE_USER) {
        folder_perms = g_hash_table_lookup (mgr->priv->user_perms, repo_id);
        if (folder_perms) {
            GList *existing = g_list_find_custom (folder_perms,
                                                  perm,
                                                  comp_folder_perms);
            if (existing) {
                FolderPerm *old_perm = existing->data;
                g_free (old_perm->permission);
                old_perm->permission = g_strdup (perm->permission);
            } else {
                FolderPerm *new_perm = folder_perm_new (perm->path, perm->permission);
                folder_perms = g_list_insert_sorted (folder_perms, new_perm,
                                                     comp_folder_perms);
            }
        } else {
                FolderPerm *new_perm = folder_perm_new (perm->path, perm->permission);
                folder_perms = g_list_insert_sorted (folder_perms, new_perm,
                                                     comp_folder_perms);
        }
        g_hash_table_insert (mgr->priv->user_perms, g_strdup(repo_id), folder_perms);
    } else if (type == FOLDER_PERM_TYPE_GROUP) {
        folder_perms = g_hash_table_lookup (mgr->priv->group_perms, repo_id);
        if (folder_perms) {
            GList *existing = g_list_find_custom (folder_perms,
                                                  perm,
                                                  comp_folder_perms);
            if (existing) {
                FolderPerm *old_perm = existing->data;
                g_free (old_perm->permission);
                old_perm->permission = g_strdup (perm->permission);
            } else {
                FolderPerm *new_perm = folder_perm_new (perm->path, perm->permission);
                folder_perms = g_list_insert_sorted (folder_perms, new_perm,
                                                     comp_folder_perms);
            }
        } else {
                FolderPerm *new_perm = folder_perm_new (perm->path, perm->permission);
                folder_perms = g_list_insert_sorted (folder_perms, new_perm,
                                                     comp_folder_perms);
        }
        g_hash_table_insert (mgr->priv->group_perms, g_strdup(repo_id), folder_perms);
    }
    pthread_mutex_unlock (&mgr->priv->perm_lock);

    return 0;
}

int
seaf_repo_manager_update_folder_perms (SeafRepoManager *mgr,
                                       const char *repo_id,
                                       FolderPermType type,
                                       GList *folder_perms)
{
    char *sql;
    sqlite3_stmt *stmt;
    GList *ptr;
    FolderPerm *perm;

    g_return_val_if_fail ((type == FOLDER_PERM_TYPE_USER ||
                           type == FOLDER_PERM_TYPE_GROUP),
                          -1);

    /* Update db. */

    pthread_mutex_lock (&mgr->priv->db_lock);

    if (type == FOLDER_PERM_TYPE_USER)
        sql = "DELETE FROM FolderUserPerms WHERE repo_id = ?";
    else
        sql = "DELETE FROM FolderGroupPerms WHERE repo_id = ?";
    stmt = sqlite_query_prepare (mgr->priv->db, sql);
    sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT);
    if (sqlite3_step (stmt) != SQLITE_DONE) {
        seaf_warning ("Failed to remove folder perms for %.8s: %s.\n",
                      repo_id, sqlite3_errmsg (mgr->priv->db));
        sqlite3_finalize (stmt);
        pthread_mutex_unlock (&mgr->priv->db_lock);
        return -1;
    }
    sqlite3_finalize (stmt);

    if (!folder_perms) {
        pthread_mutex_unlock (&mgr->priv->db_lock);
        return 0;
    }

    if (type == FOLDER_PERM_TYPE_USER)
        sql = "INSERT INTO FolderUserPerms VALUES (?, ?, ?)";
    else
        sql = "INSERT INTO FolderGroupPerms VALUES (?, ?, ?)";
    stmt = sqlite_query_prepare (mgr->priv->db, sql);

    for (ptr = folder_perms; ptr; ptr = ptr->next) {
        perm = ptr->data;

        sqlite3_bind_text (stmt, 1, repo_id, -1, SQLITE_TRANSIENT);
        sqlite3_bind_text (stmt, 2, perm->path, -1, SQLITE_TRANSIENT);
        sqlite3_bind_text (stmt, 3, perm->permission, -1, SQLITE_TRANSIENT);

        if (sqlite3_step (stmt) != SQLITE_DONE) {
            seaf_warning ("Failed to insert folder perms for %.8s: %s.\n",
                          repo_id, sqlite3_errmsg (mgr->priv->db));
            sqlite3_finalize (stmt);
            pthread_mutex_unlock (&mgr->priv->db_lock);
            return -1;
        }

        sqlite3_reset (stmt);
        sqlite3_clear_bindings (stmt);
    }

    sqlite3_finalize (stmt);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    /* Update in memory */
    GList *new, *old;
    new = folder_perm_list_copy (folder_perms);
    new = g_list_sort (new, comp_folder_perms);

    pthread_mutex_lock (&mgr->priv->perm_lock);
    if (type == FOLDER_PERM_TYPE_USER) {
        old = g_hash_table_lookup (mgr->priv->user_perms, repo_id);
        if (old)
            g_list_free_full (old, (GDestroyNotify)folder_perm_free);
        g_hash_table_insert (mgr->priv->user_perms, g_strdup(repo_id), new);
    } else if (type == FOLDER_PERM_TYPE_GROUP) {
        old = g_hash_table_lookup (mgr->priv->group_perms, repo_id);
        if (old)
            g_list_free_full (old, (GDestroyNotify)folder_perm_free);
        g_hash_table_insert (mgr->priv->group_perms, g_strdup(repo_id), new);
    }
    pthread_mutex_unlock (&mgr->priv->perm_lock);

    return 0;
}

static gboolean
load_folder_perm (sqlite3_stmt *stmt, void *data)
{
    GList **p_perms = data;
    const char *path, *permission;

    path = (const char *)sqlite3_column_text (stmt, 0);
    permission = (const char *)sqlite3_column_text (stmt, 1);

    FolderPerm *perm = folder_perm_new (path, permission);
    *p_perms = g_list_prepend (*p_perms, perm);

    return TRUE;
}

static GList *
load_folder_perms_for_repo (SeafRepoManager *mgr,
                            const char *repo_id,
                            FolderPermType type)
{
    GList *perms = NULL;
    char sql[256];

    g_return_val_if_fail ((type == FOLDER_PERM_TYPE_USER ||
                           type == FOLDER_PERM_TYPE_GROUP),
                          NULL);

    if (type == FOLDER_PERM_TYPE_USER)
        sqlite3_snprintf (sizeof(sql), sql,
                          "SELECT path, permission FROM FolderUserPerms "
                          "WHERE repo_id = '%q'",
                          repo_id);
    else
        sqlite3_snprintf (sizeof(sql), sql,
                          "SELECT path, permission FROM FolderGroupPerms "
                          "WHERE repo_id = '%q'",
                          repo_id);

    pthread_mutex_lock (&mgr->priv->db_lock);

    if (sqlite_foreach_selected_row (mgr->priv->db, sql,
                                     load_folder_perm, &perms) < 0) {
        pthread_mutex_unlock (&mgr->priv->db_lock);
        GList *ptr;
        for (ptr = perms; ptr; ptr = ptr->next)
            folder_perm_free ((FolderPerm *)ptr->data);
        g_list_free (perms);
        return NULL;
    }

    pthread_mutex_unlock (&mgr->priv->db_lock);

    /* Sort list in descending order by perm->path (longer path first). */
    perms = g_list_sort (perms, comp_folder_perms);

    return perms;
}

static void
init_folder_perms (SeafRepoManager *mgr)
{
    SeafRepoManagerPriv *priv = mgr->priv;
    GList *repo_ids = g_hash_table_get_keys (priv->repo_hash);
    GList *ptr;
    GList *perms;
    char *repo_id;

    for (ptr = repo_ids; ptr; ptr = ptr->next) {
        repo_id = ptr->data;
        perms = load_folder_perms_for_repo (mgr, repo_id, FOLDER_PERM_TYPE_USER);
        if (perms) {
            pthread_mutex_lock (&priv->perm_lock);
            g_hash_table_insert (priv->user_perms, g_strdup(repo_id), perms);
            pthread_mutex_unlock (&priv->perm_lock);
        }
        perms = load_folder_perms_for_repo (mgr, repo_id, FOLDER_PERM_TYPE_GROUP);
        if (perms) {
            pthread_mutex_lock (&priv->perm_lock);
            g_hash_table_insert (priv->group_perms, g_strdup(repo_id), perms);
            pthread_mutex_unlock (&priv->perm_lock);
        }
    }

    g_list_free (repo_ids);
}

static void
remove_folder_perms (SeafRepoManager *mgr, const char *repo_id)
{
    GList *perms = NULL;

    pthread_mutex_lock (&mgr->priv->perm_lock);

    perms = g_hash_table_lookup (mgr->priv->user_perms, repo_id);
    if (perms) {
        g_list_free_full (perms, (GDestroyNotify)folder_perm_free);
        g_hash_table_remove (mgr->priv->user_perms, repo_id);
    }

    perms = g_hash_table_lookup (mgr->priv->group_perms, repo_id);
    if (perms) {
        g_list_free_full (perms, (GDestroyNotify)folder_perm_free);
        g_hash_table_remove (mgr->priv->group_perms, repo_id);
    }

    pthread_mutex_unlock (&mgr->priv->perm_lock);
}

int
seaf_repo_manager_update_folder_perm_timestamp (SeafRepoManager *mgr,
                                                const char *repo_id,
                                                gint64 timestamp)
{
    char sql[256];
    int ret;

    snprintf (sql, sizeof(sql),
              "REPLACE INTO FolderPermTimestamp VALUES ('%s', %"G_GINT64_FORMAT")",
              repo_id, timestamp);

    pthread_mutex_lock (&mgr->priv->db_lock);

    ret = sqlite_query_exec (mgr->priv->db, sql);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    return ret;
}

gint64
seaf_repo_manager_get_folder_perm_timestamp (SeafRepoManager *mgr,
                                             const char *repo_id)
{
    char sql[256];
    gint64 ret;

    sqlite3_snprintf (sizeof(sql), sql,
                      "SELECT timestamp FROM FolderPermTimestamp WHERE repo_id = '%q'",
                      repo_id);

    pthread_mutex_lock (&mgr->priv->db_lock);

    ret = sqlite_get_int64 (mgr->priv->db, sql);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    return ret;
}

static char *
lookup_folder_perm (GList *perms, const char *path)
{
    GList *ptr;
    FolderPerm *perm;
    char *folder;
    int len;
    char *permission = NULL;

    for (ptr = perms; ptr; ptr = ptr->next) {
        perm = ptr->data;

        if (strcmp (perm->path, "/") != 0)
            folder = g_strconcat (perm->path, "/", NULL);
        else
            folder = g_strdup(perm->path);

        len = strlen(folder);
        if (strcmp (perm->path, path) == 0 || strncmp(folder, path, len) == 0) {
            permission = perm->permission;
            g_free (folder);
            break;
        }
        g_free (folder);
    }

    return permission;
}

static gboolean
is_path_writable (const char *repo_id,
                  gboolean is_repo_readonly,
                  const char *path)
{
    SeafRepoManager *mgr = seaf->repo_mgr;
    GList *user_perms = NULL, *group_perms = NULL;
    char *permission = NULL;
    char *abs_path = NULL;

    pthread_mutex_lock (&mgr->priv->perm_lock);

    user_perms = g_hash_table_lookup (mgr->priv->user_perms, repo_id);
    group_perms = g_hash_table_lookup (mgr->priv->group_perms, repo_id);

    if (user_perms || group_perms)
        abs_path = g_strconcat ("/", path, NULL);

    if (user_perms)
        permission = lookup_folder_perm (user_perms, abs_path);
    if (!permission && group_perms)
        permission = lookup_folder_perm (group_perms, abs_path);

    pthread_mutex_unlock (&mgr->priv->perm_lock);

    g_free (abs_path);

    if (!permission)
        return !is_repo_readonly;

    if (strcmp (permission, "rw") == 0)
        return TRUE;
    else
        return FALSE;
}

gboolean
seaf_repo_manager_is_path_writable (SeafRepoManager *mgr,
                                    const char *repo_id,
                                    const char *path)
{
    SeafRepo *repo = seaf_repo_manager_get_repo (mgr, repo_id);
    if (!repo) {
        seaf_warning ("Failed to get repo %s.\n", repo_id);
        return FALSE;
    }

    return is_path_writable (repo_id, repo->is_readonly, path);
}

gboolean
is_repo_id_valid (const char *id)
{
    if (!id)
        return FALSE;

    return is_uuid_valid (id);
}

/*
 * Sync error related. These functions should belong to the sync-mgr module.
 * But since we have to store the errors in repo database, we have to put the code here.
 */

int
seaf_repo_manager_record_sync_error (const char *repo_id,
                                     const char *repo_name,
                                     const char *path,
                                     int error_id)
{
    char *sql;
    int ret;

    pthread_mutex_lock (&seaf->repo_mgr->priv->db_lock);

    if (path != NULL)
        sql = sqlite3_mprintf ("DELETE FROM FileSyncError WHERE repo_id='%q' AND path='%q'",
                               repo_id, path);
    else
        sql = sqlite3_mprintf ("DELETE FROM FileSyncError WHERE repo_id='%q' AND path IS NULL",
                               repo_id);
    ret = sqlite_query_exec (seaf->repo_mgr->priv->db, sql);
    sqlite3_free (sql);
    if (ret < 0)
        goto out;

    /* REPLACE INTO will update the primary key id automatically.
     * So new errors are always on top.
     */
    if (path != NULL)
        sql = sqlite3_mprintf ("INSERT INTO FileSyncError "
                               "(repo_id, repo_name, path, err_id, timestamp) "
                               "VALUES ('%q', '%q', '%q', %d, %lld)",
                               repo_id, repo_name, path, error_id, (gint64)time(NULL));
    else
        sql = sqlite3_mprintf ("INSERT INTO FileSyncError "
                               "(repo_id, repo_name, err_id, timestamp) "
                               "VALUES ('%q', '%q', %d, %lld)",
                               repo_id, repo_name, error_id, (gint64)time(NULL));
        
    ret = sqlite_query_exec (seaf->repo_mgr->priv->db, sql);
    sqlite3_free (sql);

out:
    pthread_mutex_unlock (&seaf->repo_mgr->priv->db_lock);
    return ret;
}

static gboolean
collect_file_sync_errors (sqlite3_stmt *stmt, void *data)
{
    GList **pret = data;
    const char *repo_id, *repo_name, *path;
    int id, err_id;
    gint64 timestamp;
    SeafileFileSyncError *error;

    id = sqlite3_column_int (stmt, 0);
    repo_id = (const char *)sqlite3_column_text (stmt, 1);
    repo_name = (const char *)sqlite3_column_text (stmt, 2);
    path = (const char *)sqlite3_column_text (stmt, 3);
    err_id = sqlite3_column_int (stmt, 4);
    timestamp = sqlite3_column_int64 (stmt, 5);

    error = g_object_new (SEAFILE_TYPE_FILE_SYNC_ERROR,
                          "id", id,
                          "repo_id", repo_id,
                          "repo_name", repo_name,
                          "path", path,
                          "err_id", err_id,
                          "timestamp", timestamp,
                          NULL);
    *pret = g_list_prepend (*pret, error);

    return TRUE;
}

int
seaf_repo_manager_del_file_sync_error_by_id (SeafRepoManager *mgr, int id)
{
    int ret = 0;    
    char *sql = NULL;

    pthread_mutex_lock (&mgr->priv->db_lock);

    sql = sqlite3_mprintf ("DELETE FROM FileSyncError WHERE id=%d",
                           id);
    ret = sqlite_query_exec (mgr->priv->db, sql);
    sqlite3_free (sql);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    return ret;
}

GList *
seaf_repo_manager_get_file_sync_errors (SeafRepoManager *mgr, int offset, int limit)
{
    GList *ret = NULL;
    char *sql;

    pthread_mutex_lock (&mgr->priv->db_lock);

    sql = sqlite3_mprintf ("SELECT id, repo_id, repo_name, path, err_id, timestamp FROM "
                           "FileSyncError ORDER BY id DESC LIMIT %d OFFSET %d",
                           limit, offset);
    sqlite_foreach_selected_row (mgr->priv->db, sql,
                                 collect_file_sync_errors, &ret);
    sqlite3_free (sql);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    ret = g_list_reverse (ret);

    return ret;
}

/*
 * Record file-level sync errors and send system notification.
 */

struct _SyncError {
    char *repo_id;
    char *repo_name;
    char *path;
    int err_id;
    gint64 timestamp;
};
typedef struct _SyncError SyncError;

void
send_sync_error_notification (const char *repo_id,
                              const char *repo_name,
                              const char *path,
                              int err_id)
{
    json_t *object;
    char *str;

    object = json_object ();
    json_object_set_new (object, "repo_id", json_string(repo_id));
    json_object_set_new (object, "repo_name", json_string(repo_name));
    json_object_set_new (object, "path", json_string(path));
    json_object_set_new (object, "err_id", json_integer(err_id));

    str = json_dumps (object, 0);

    seaf_mq_manager_publish_notification (seaf->mq_mgr,
                                          "sync.error",
                                          str);

    free (str);
    json_decref (object);

}

static void
check_and_send_notification (SeafRepoManager *mgr,
                             const char *repo_id,
                             const char *repo_name,
                             const char *path,
                             int err_id)
{
    GList *errors = mgr->priv->sync_errors, *ptr;
    SyncError *err, *new_err;
    gboolean found = FALSE;

    pthread_mutex_lock (&mgr->priv->errors_lock);

    for (ptr = errors; ptr; ptr = ptr->next) {
        err = ptr->data;
        if (g_strcmp0 (err->repo_id, repo_id) == 0 &&
            g_strcmp0 (err->path, path) == 0) {
            found = TRUE;
            if (err->err_id != err_id) {
                err->err_id = err_id;
                send_sync_error_notification (repo_id, repo_name, path, err_id);
            }
            err->timestamp = (gint64)time(NULL);
            break;
        }
    }

    if (!found) {
        new_err = g_new0 (SyncError, 1);
        new_err->repo_id = g_strdup(repo_id);
        new_err->repo_name = g_strdup(repo_name);
        new_err->path = g_strdup(path);
        new_err->err_id = err_id;
        new_err->timestamp = (gint64)time(NULL);
        mgr->priv->sync_errors = g_list_prepend (mgr->priv->sync_errors, new_err);

        send_sync_error_notification (repo_id, repo_name, path, err_id);
    }

    pthread_mutex_unlock (&mgr->priv->errors_lock);
}

void
send_file_sync_error_notification (const char *repo_id,
                                   const char *repo_name,
                                   const char *path,
                                   int err_id)
{
    if (!repo_name) {
        SeafRepo *repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id);
        if (!repo)
            return;
        repo_name = repo->name;
    }

    seaf_repo_manager_record_sync_error (repo_id, repo_name, path, err_id);

    seaf_sync_manager_set_task_error_code (seaf->sync_mgr, repo_id, err_id);

    check_and_send_notification (seaf->repo_mgr, repo_id, repo_name, path, err_id);
}

SeafRepo*
seaf_repo_new (const char *id, const char *name, const char *desc)
{
    SeafRepo* repo;

    /* valid check */
  
    
    repo = g_new0 (SeafRepo, 1);
    memcpy (repo->id, id, 36);
    repo->id[36] = '\0';

    repo->name = g_strdup(name);
    repo->desc = g_strdup(desc);

    repo->worktree_invalid = TRUE;
    repo->auto_sync = 1;
    pthread_mutex_init (&repo->lock, NULL);

    return repo;
}

int
seaf_repo_check_worktree (SeafRepo *repo)
{
    SeafStat st;

    if (repo->worktree == NULL) {
        seaf_warning ("Worktree for repo '%s'(%.8s) is not set.\n",
                      repo->name, repo->id);
        return -1;
    }

    /* check repo worktree */
    if (g_access(repo->worktree, F_OK) < 0) {
        if (!repo->worktree_invalid) {
            seaf_warning ("Failed to access worktree %s for repo '%s'(%.8s)\n",
                          repo->worktree, repo->name, repo->id);
        }

        return -1;
    }

    if (seaf_stat(repo->worktree, &st) < 0) {
        seaf_warning ("Failed to stat worktree %s for repo '%s'(%.8s)\n",
                      repo->worktree, repo->name, repo->id);
        return -1;
    }
    if (!S_ISDIR(st.st_mode)) {
        seaf_warning ("Worktree %s for repo '%s'(%.8s) is not a directory.\n",
                      repo->worktree, repo->name, repo->id);
        return -1;
    }

    return 0;
}


static gboolean
check_worktree_common (SeafRepo *repo)
{
    if (!repo->head) {
        seaf_warning ("Head for repo '%s'(%.8s) is not set.\n",
                      repo->name, repo->id);
        return FALSE;
    }

    if (seaf_repo_check_worktree (repo) < 0) {
        return FALSE;
    }

    return TRUE;
}

void
seaf_repo_free (SeafRepo *repo)
{
    if (repo->head) seaf_branch_unref (repo->head);

    g_free (repo->name);
    g_free (repo->desc);
    g_free (repo->category);
    g_free (repo->worktree);
    g_free (repo->relay_id);
    g_free (repo->email);
    g_free (repo->username);
    g_free (repo->token);
    g_free (repo->pwd_hash_algo);
    g_free (repo->pwd_hash_params);
    g_free (repo);
}

static void
set_head_common (SeafRepo *repo, SeafBranch *branch)
{
    if (repo->head)
        seaf_branch_unref (repo->head);
    repo->head = branch;
    seaf_branch_ref(branch);
}

int
seaf_repo_set_head (SeafRepo *repo, SeafBranch *branch)
{
    if (save_branch_repo_map (repo->manager, branch) < 0)
        return -1;
    set_head_common (repo, branch);
    return 0;
}

SeafCommit *
seaf_repo_get_head_commit (const char *repo_id)
{
    SeafRepo *repo;
    SeafCommit *head;

    repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id);
    if (!repo) {
        seaf_warning ("Failed to get repo %s.\n", repo_id);
        return NULL;
    }

    head = seaf_commit_manager_get_commit (seaf->commit_mgr,
                                           repo_id, repo->version,
                                           repo->head->commit_id);
    if (!head) {
        seaf_warning ("Failed to get head for repo %s.\n", repo_id);
        return NULL;
    }

    return head;
}

void
seaf_repo_from_commit (SeafRepo *repo, SeafCommit *commit)
{
    repo->name = g_strdup (commit->repo_name);
    repo->desc = g_strdup (commit->repo_desc);
    repo->encrypted = commit->encrypted;
    repo->last_modify = commit->ctime;
    memcpy (repo->root_id, commit->root_id, 40);
    if (repo->encrypted) {
        repo->enc_version = commit->enc_version;
        if (repo->enc_version == 1 && !commit->pwd_hash_algo)
            memcpy (repo->magic, commit->magic, 32);
        else if (repo->enc_version == 2) {
            memcpy (repo->random_key, commit->random_key, 96);
        }
        else if (repo->enc_version == 3) {
            memcpy (repo->random_key, commit->random_key, 96);
            memcpy (repo->salt, commit->salt, 64);
        }
        else if (repo->enc_version == 4) {
            memcpy (repo->random_key, commit->random_key, 96);
            memcpy (repo->salt, commit->salt, 64);
        }
        if (repo->enc_version >= 2 && !commit->pwd_hash_algo) {
            memcpy (repo->magic, commit->magic, 64);
        }
        if (commit->pwd_hash_algo) {
            memcpy (repo->pwd_hash, commit->pwd_hash, 64);
            repo->pwd_hash_algo = g_strdup (commit->pwd_hash_algo);
            repo->pwd_hash_params = g_strdup (commit->pwd_hash_params);
        }
    }
    repo->no_local_history = commit->no_local_history;
    repo->version = commit->version;
}

void
seaf_repo_to_commit (SeafRepo *repo, SeafCommit *commit)
{
    commit->repo_name = g_strdup (repo->name);
    commit->repo_desc = g_strdup (repo->desc);
    commit->encrypted = repo->encrypted;
    if (commit->encrypted) {
        commit->enc_version = repo->enc_version;
        if (commit->enc_version == 1 && !repo->pwd_hash_algo)
            commit->magic = g_strdup (repo->magic);
        else if (commit->enc_version == 2) {
            commit->random_key = g_strdup (repo->random_key);
        }
        else if (commit->enc_version == 3) {
            commit->random_key = g_strdup (repo->random_key);
            commit->salt = g_strdup (repo->salt);
        }
        else if (commit->enc_version == 4) {
            commit->random_key = g_strdup (repo->random_key);
            commit->salt = g_strdup (repo->salt);
        }
        if (commit->enc_version >= 2 && !repo->pwd_hash_algo) {
            commit->magic = g_strdup (repo->magic);
        }
        if (repo->pwd_hash_algo) {
            commit->pwd_hash = g_strdup (repo->pwd_hash);
            commit->pwd_hash_algo = g_strdup (repo->pwd_hash_algo);
            commit->pwd_hash_params = g_strdup (repo->pwd_hash_params);
        }
    }
    commit->no_local_history = repo->no_local_history;
    commit->version = repo->version;
}

static gboolean
need_to_sync_worktree_name (const char *repo_id)
{
    char *need_sync_wt_name = seaf_repo_manager_get_repo_property (seaf->repo_mgr,
                                                                   repo_id,
                                                                   REPO_SYNC_WORKTREE_NAME);
    gboolean ret = (g_strcmp0(need_sync_wt_name, "true") == 0);
    g_free (need_sync_wt_name);
    return ret;
}

static void
update_repo_worktree_name (SeafRepo *repo, const char *new_name, gboolean rewatch)
{
    char *dirname = NULL, *basename = NULL;
    char *new_worktree = NULL;

    seaf_message ("Update worktree folder name of repo %s to %s.\n",
                  repo->id, new_name);

    dirname = g_path_get_dirname (repo->worktree);
    if (g_strcmp0 (dirname, ".") == 0)
        return;
    basename = g_path_get_basename (repo->worktree);

    new_worktree = g_build_filename (dirname, new_name, NULL);

    /* This can possibly fail on Windows if some files are opened under the worktree.
     * The rename operation will be retried on next restart.
     */
    if (seaf_util_rename (repo->worktree, new_worktree) < 0) {
        seaf_warning ("Failed to rename worktree from %s to %s: %s.\n",
                      repo->worktree, new_worktree, strerror(errno));
        goto out;
    }

    if (seaf_repo_manager_set_repo_worktree (seaf->repo_mgr, repo, new_worktree) < 0) {
        goto out;
    }

    if (rewatch) {
        if (seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id) < 0) {
            seaf_warning ("Failed to unwatch repo %s old worktree.\n", repo->id);
            goto out;
        }

        if (seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree) < 0) {
            seaf_warning ("Failed to watch repo %s new worktree.\n", repo->id);
        }
    }

out:
    g_free (dirname);
    g_free (basename);
    g_free (new_worktree);
}

void
seaf_repo_set_name (SeafRepo *repo, const char *new_name)
{
    char *old_name = repo->name;
    repo->name = g_strdup(new_name);
    g_free (old_name);

    if (need_to_sync_worktree_name (repo->id))
        update_repo_worktree_name (repo, new_name, TRUE);
}

static gboolean
collect_commit (SeafCommit *commit, void *vlist, gboolean *stop)
{
    GList **commits = vlist;

    /* The traverse function will unref the commit, so we need to ref it.
     */
    seaf_commit_ref (commit);
    *commits = g_list_prepend (*commits, commit);
    return TRUE;
}

GList *
seaf_repo_get_commits (SeafRepo *repo)
{
    GList *branches;
    GList *ptr;
    SeafBranch *branch;
    GList *commits = NULL;

    branches = seaf_branch_manager_get_branch_list (seaf->branch_mgr, repo->id);
    if (branches == NULL) {
        seaf_warning ("Failed to get branch list of repo %s.\n", repo->id);
        return NULL;
    }

    for (ptr = branches; ptr != NULL; ptr = ptr->next) {
        branch = ptr->data;
        gboolean res = seaf_commit_manager_traverse_commit_tree (seaf->commit_mgr,
                                                                 repo->id,
                                                                 repo->version,
                                                                 branch->commit_id,
                                                                 collect_commit,
                                                                 &commits, FALSE);
        if (!res) {
            for (ptr = commits; ptr != NULL; ptr = ptr->next)
                seaf_commit_unref ((SeafCommit *)(ptr->data));
            g_list_free (commits);
            goto out;
        }
    }

    commits = g_list_reverse (commits);

out:
    for (ptr = branches; ptr != NULL; ptr = ptr->next) {
        seaf_branch_unref ((SeafBranch *)ptr->data);
    }
    return commits;
}

void
seaf_repo_set_readonly (SeafRepo *repo)
{
    repo->is_readonly = TRUE;
    save_repo_property (repo->manager, repo->id, REPO_PROP_IS_READONLY, "true");
}

void
seaf_repo_unset_readonly (SeafRepo *repo)
{
    repo->is_readonly = FALSE;
    save_repo_property (repo->manager, repo->id, REPO_PROP_IS_READONLY, "false");
}

gboolean
seaf_repo_manager_is_ignored_hidden_file (const char *filename)
{
    GPatternSpec **spec = ignore_patterns;

    while (*spec) {
        if (g_pattern_match_string(*spec, filename))
            return TRUE;
        spec++;
    }

    return FALSE;
}

static gboolean
should_ignore(const char *basepath, const char *filename, void *data)
{
    GPatternSpec **spec = ignore_patterns;
    GList *ignore_list = (GList *)data;

    if (!g_utf8_validate (filename, -1, NULL)) {
        seaf_warning ("File name %s contains non-UTF8 characters, skip.\n", filename);
        return TRUE;
    }

    /* Ignore file/dir if its name is too long. */
    if (strlen(filename) >= SEAF_DIR_NAME_LEN) {
        seaf_warning ("File name %s is too long, skip.\n", filename);
        return TRUE;
    }

    if (strchr (filename, '/'))
        return TRUE;

    while (*spec) {
        if (g_pattern_match_string(*spec, filename))
            return TRUE;
        spec++;
    }

    if (!seaf->sync_extra_temp_file) {
        spec = office_temp_ignore_patterns;
        while (*spec) {
            if (g_pattern_match_string(*spec, filename))
                return TRUE;
            spec++;
        }
    }

    if (basepath) {
        char *fullpath = g_build_path ("/", basepath, filename, NULL);
        if (seaf_repo_check_ignore_file (ignore_list, fullpath)) {
            g_free (fullpath);
            return TRUE;
        }
        g_free (fullpath);
    }

    return FALSE;
}

#ifndef WIN32
static inline gboolean
has_trailing_space_or_period (const char *path)
{
    int len = strlen(path);
    if (path[len - 1] == ' ' || path[len - 1] == '.') {
        return TRUE;
    }

    return FALSE;
}

static gboolean
check_path_ignore_on_windows (const char *file_path)
{
    gboolean ret = FALSE;
    static char illegals[] = {'\\', ':', '*', '?', '"', '<', '>', '|', '\b', '\t'};
    char **components = g_strsplit (file_path, "/", -1);
    int n_comps = g_strv_length (components);
    int j = 0;
    char *file_name;
    int i;
    char c;

    for (; j < n_comps; ++j) {
        file_name = components[j];

        if (has_trailing_space_or_period (file_name)) {
            /* Ignore files/dir whose path has trailing spaces. It would cause
             * problem on windows. */
            /* g_debug ("ignore '%s' which contains trailing space in path\n", path); */
            ret = TRUE;
            goto out;
        }

        for (i = 0; i < G_N_ELEMENTS(illegals); i++) {
            if (strchr (file_name, illegals[i])) {
                ret = TRUE;
                goto out;
            }
        }

        for (c = 1; c <= 31; c++) {
            if (strchr (file_name, c)) {
                ret = TRUE;
                goto out;
            }
        }
    }

out:
    g_strfreev (components);

    return ret;
}
#endif

static int
index_cb (const char *repo_id,
          int version,
          const char *path,
          unsigned char sha1[],
          SeafileCrypt *crypt,
          gboolean write_data)
{
    gint64 size;

    /* Check in blocks and get object ID. */
    if (seaf_fs_manager_index_blocks (seaf->fs_mgr, repo_id, version,
                                      path, sha1, &size, crypt, write_data, !seaf->disable_block_hash) < 0) {
        seaf_warning ("Failed to index file %s.\n", path);
        return -1;
    }
    return 0;
}

static gboolean
is_symlink (const char *full_path)
{
#ifndef WIN32
    SeafStat st;

    if (lstat (full_path, &st) == 0 && S_ISLNK(st.st_mode)) {
        return TRUE;
    }
    return FALSE;
#else
    return FALSE;
#endif
}

#define MAX_COMMIT_SIZE 100 * (1 << 20) /* 100MB */

typedef struct _AddOptions {
    LockedFileSet *fset;
    ChangeSet *changeset;
    gboolean is_repo_ro;
    gboolean startup_scan;
} AddOptions;

static int
add_file (const char *repo_id,
          int version,
          const char *modifier,
          struct index_state *istate, 
          const char *path,
          const char *full_path,
          SeafStat *st,
          SeafileCrypt *crypt,
          gint64 *total_size,
          GQueue **remain_files,
          AddOptions *options)
{
    gboolean added = FALSE;
    int ret = 0;
    gboolean is_writable = TRUE, is_locked = FALSE;
    struct cache_entry *ce;
    char *base_name = NULL;

    if (seaf->ignore_symlinks && is_symlink(full_path)) {
        return ret;
    }

    if (options)
        is_writable = is_path_writable(repo_id,
                                       options->is_repo_ro, path);

    is_locked = seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                                      repo_id, path);
    if (is_locked && options && !(options->startup_scan)) {
        /* send_sync_error_notification (repo_id, NULL, path, */
        /*                               SYNC_ERROR_ID_FILE_LOCKED); */
    }

    if (options && options->startup_scan) {
        SyncStatus status;

        ce = index_name_exists (istate, path, strlen(path), 0);
        if (!ce || ie_match_stat(ce, st, 0) != 0)
            status = SYNC_STATUS_SYNCING;
        else
            status = SYNC_STATUS_SYNCED;

        /* Don't set "syncing" status for read-only path. */
        if (status == SYNC_STATUS_SYNCED || (is_writable && !is_locked))
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  repo_id,
                                                  path,
                                                  S_IFREG,
                                                  status,
                                                  FALSE);
        /* send an error notification for read-only repo when modifying a file. */
        if (status == SYNC_STATUS_SYNCING && !is_writable)
            send_file_sync_error_notification (repo_id, NULL, path,
                                               SYNC_ERROR_ID_UPDATE_TO_READ_ONLY_REPO);
    }

    if (!is_writable || is_locked)
        return ret;

#if defined WIN32 || defined __APPLE__
    if (options && options->fset) {
        LockedFile *file = locked_file_set_lookup (options->fset, path);
        if (file) {
            if (strcmp (file->operation, LOCKED_OP_DELETE) == 0) {
                /* Only remove the lock record if the file is changed. */
                if (st->st_mtime == file->old_mtime) {
                    return ret;
                }
                locked_file_set_remove (options->fset, path, FALSE);
            } else if (strcmp (file->operation, LOCKED_OP_UPDATE) == 0) {
                return ret;
            }
        }
    }
#endif

#ifndef WIN32
    base_name = g_path_get_basename(full_path);
    if (!seaf->hide_windows_incompatible_path_notification &&
        check_path_ignore_on_windows (base_name)) {

        ce = index_name_exists (istate, path, strlen(path), 0);
        if (!ce)
            send_file_sync_error_notification (repo_id, NULL, path,
                                               SYNC_ERROR_ID_INVALID_PATH_ON_WINDOWS);
    }
    g_free (base_name);
#endif

    if (!remain_files) {
        ret = add_to_index (repo_id, version, istate, path, full_path,
                            st, 0, crypt, index_cb, modifier, &added);
        if (!added) {
            /* If the contents of the file doesn't change, move it to
               synced status.
            */
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  repo_id,
                                                  path,
                                                  S_IFREG,
                                                  SYNC_STATUS_SYNCED,
                                                  FALSE);
        } else {
            if (total_size)
                *total_size += (gint64)(st->st_size);
            if (options && options->changeset) {
                /* ce may be updated. */
                ce = index_name_exists (istate, path, strlen(path), 0);
                add_to_changeset (options->changeset,
                                  DIFF_STATUS_ADDED,
                                  ce->sha1,
                                  st,
                                  modifier,
                                  path,
                                  NULL);
            }
        }
    } else if (*remain_files == NULL) {
        ret = add_to_index (repo_id, version, istate, path, full_path,
                            st, 0, crypt, index_cb, modifier, &added);
        if (added) {
            *total_size += (gint64)(st->st_size);
            if (*total_size >= MAX_COMMIT_SIZE)
                *remain_files = g_queue_new ();
        } else {
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  repo_id,
                                                  path,
                                                  S_IFREG,
                                                  SYNC_STATUS_SYNCED,
                                                  FALSE);
        }
        if (added && options && options->changeset) {
            /* ce may be updated. */
            ce = index_name_exists (istate, path, strlen(path), 0);
            add_to_changeset (options->changeset,
                              DIFF_STATUS_ADDED,
                              ce->sha1,
                              st,
                              modifier,
                              path,
                              NULL);
        }
    } else {
        *total_size += (gint64)(st->st_size);
        g_queue_push_tail (*remain_files, g_strdup(path));
    }

    if (ret < 0) {
        seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                              repo_id,
                                              path,
                                              S_IFREG,
                                              SYNC_STATUS_ERROR,
                                              TRUE);
        // Only record index error when the file exists.
        if (seaf_util_exists (full_path)) {
            send_file_sync_error_notification (repo_id, NULL, path,
                                               SYNC_ERROR_ID_INDEX_ERROR);
        }
    }

    return ret;
}

typedef struct AddParams {
    const char *repo_id;
    int version;
    const char *modifier;
    struct index_state *istate;
    const char *worktree;
    SeafileCrypt *crypt;
    gboolean ignore_empty_dir;
    GList *ignore_list;
    gint64 *total_size;
    GQueue **remain_files;
    AddOptions *options;
} AddParams;

#ifndef WIN32

static int
add_dir_recursive (const char *path, const char *full_path, SeafStat *st,
                   AddParams *params, gboolean ignored)
{
    AddOptions *options = params->options;
    GDir *dir;
    const char *dname;
    char *subpath, *full_subpath;
    int n, total;
    gboolean is_writable = TRUE;
    struct stat sub_st;
    char *base_name = NULL;

    if (seaf->ignore_symlinks && is_symlink(full_path)) {
        return 0;
    }

    dir = g_dir_open (full_path, 0, NULL);
    if (!dir) {
        seaf_warning ("Failed to open dir %s: %s.\n", full_path, strerror(errno));

        seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                              params->repo_id,
                                              path,
                                              S_IFDIR,
                                              SYNC_STATUS_ERROR,
                                              TRUE);
        if (!ignored) {
            send_file_sync_error_notification (params->repo_id, NULL, path,
                                               SYNC_ERROR_ID_INDEX_ERROR);
        }
        return 0;
    }

    base_name = g_path_get_basename(full_path);
    if (!seaf->hide_windows_incompatible_path_notification &&
        check_path_ignore_on_windows (base_name)) {

        struct cache_entry *ce = index_name_exists (params->istate, path,
                                                    strlen(path), 0);
        if (!ce)
            send_file_sync_error_notification (params->repo_id, NULL, path,
                                               SYNC_ERROR_ID_INVALID_PATH_ON_WINDOWS);
    }
    g_free (base_name);

    n = 0;
    total = 0;
    while ((dname = g_dir_read_name(dir)) != NULL) {
        ++total;

#ifdef __APPLE__
        char *norm_dname = g_utf8_normalize (dname, -1, G_NORMALIZE_NFC);
        subpath = g_build_path (PATH_SEPERATOR, path, norm_dname, NULL);
        g_free (norm_dname);
#else
        subpath = g_build_path (PATH_SEPERATOR, path, dname, NULL);
#endif
        full_subpath = g_build_filename (params->worktree, subpath, NULL);

        if (stat (full_subpath, &sub_st) < 0) {
            seaf_warning ("Failed to stat %s: %s.\n", full_subpath, strerror(errno));
            g_free (subpath);
            g_free (full_subpath);
            continue;
        }

        if (ignored || should_ignore(full_path, dname, params->ignore_list)) {
            if (options && options->startup_scan) {
                if (S_ISDIR(sub_st.st_mode))
                    add_dir_recursive (subpath, full_subpath, &sub_st, params, TRUE);
                else
                    seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                          params->repo_id,
                                                          subpath,
                                                          S_IFREG,
                                                          SYNC_STATUS_IGNORED,
                                                          TRUE);
            }
            g_free (subpath);
            g_free (full_subpath);
            continue;
        }

        ++n;

        if (S_ISDIR(sub_st.st_mode))
            add_dir_recursive (subpath, full_subpath, &sub_st, params, FALSE);
        else if (S_ISREG(sub_st.st_mode))
            add_file (params->repo_id,
                      params->version,
                      params->modifier,
                      params->istate,
                      subpath,
                      full_subpath,
                      &sub_st,
                      params->crypt,
                      params->total_size,
                      params->remain_files,
                      params->options);

        g_free (subpath);
        g_free (full_subpath);
    }
    g_dir_close (dir);

    if (ignored) {
        seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                              params->repo_id,
                                              path,
                                              S_IFDIR,
                                              SYNC_STATUS_IGNORED,
                                              TRUE);
        return 0;
    }

    if (options)
        is_writable = is_path_writable(params->repo_id,
                                       options->is_repo_ro, path);

    /* Update active path status for empty dir */
    if (options && options->startup_scan && total == 0) {
        SyncStatus status;
        struct cache_entry *ce = index_name_exists (params->istate, path,
                                                    strlen(path), 0);
        if (!ce)
            status = SYNC_STATUS_SYNCING;
        else
            status = SYNC_STATUS_SYNCED;


        if (status == SYNC_STATUS_SYNCED || is_writable)
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  params->repo_id,
                                                  path,
                                                  S_IFDIR,
                                                  status,
                                                  FALSE);
    }

    if (n == 0 && path[0] != 0 && is_writable) {
        if (!params->remain_files || *(params->remain_files) == NULL) {
            int rc = add_empty_dir_to_index (params->istate, path, st);
            if (rc == 1 && options && options->changeset) {
                unsigned char allzero[20] = {0};
                add_to_changeset (options->changeset,
                                  DIFF_STATUS_DIR_ADDED,
                                  allzero,
                                  st,
                                  NULL,
                                  path,
                                  NULL);
            }
        } else
            g_queue_push_tail (*(params->remain_files), g_strdup(path));
    }

    return 0;
}

/*
 * @remain_files: returns the files haven't been added under this path.
 *                If it's set to NULL, no partial commit will be created.
 */
static int
add_recursive (const char *repo_id,
               int version,
               const char *modifier,
               struct index_state *istate, 
               const char *worktree,
               const char *path,
               SeafileCrypt *crypt,
               gboolean ignore_empty_dir,
               GList *ignore_list,
               gint64 *total_size,
               GQueue **remain_files,
               AddOptions *options)
{
    char *full_path;
    SeafStat st;

    full_path = g_build_path (PATH_SEPERATOR, worktree, path, NULL);
    if (seaf_stat (full_path, &st) < 0) {
        /* Ignore broken symlinks on Linux and Mac OS X */
        if (lstat (full_path, &st) == 0 && S_ISLNK(st.st_mode)) {
            g_free (full_path);
            return 0;
        }
        seaf_warning ("Failed to stat %s.\n", full_path);
        g_free (full_path);
        /* Ignore error. */

        seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                              repo_id,
                                              path,
                                              0,
                                              SYNC_STATUS_ERROR,
                                              TRUE);

        return 0;
    }

    if (S_ISREG(st.st_mode)) {
        add_file (repo_id,
                  version,
                  modifier,
                  istate,
                  path,
                  full_path,
                  &st,
                  crypt,
                  total_size,
                  remain_files,
                  options);
    } else if (S_ISDIR(st.st_mode)) {
        AddParams params = {
            .repo_id = repo_id,
            .version = version,
            .modifier = modifier,
            .istate = istate,
            .worktree = worktree,
            .crypt = crypt,
            .ignore_empty_dir = ignore_empty_dir,
            .ignore_list = ignore_list,
            .total_size = total_size,
            .remain_files = remain_files,
            .options = options,
        };

        add_dir_recursive (path, full_path, &st, &params, FALSE);
    }

    g_free (full_path);
    return 0;
}

static gboolean
is_empty_dir (const char *path, GList *ignore_list)
{
    GDir *dir;
    const char *dname;
    gboolean ret = TRUE;

    dir = g_dir_open (path, 0, NULL);
    if (!dir) {
        return FALSE;
    }

    while ((dname = g_dir_read_name(dir)) != NULL) {
        if (!should_ignore(path, dname, ignore_list)) {
            ret = FALSE;
            break;
        }
    }
    g_dir_close (dir);

    return ret;
}

#else

typedef struct IterCBData {
    AddParams *add_params;
    const char *parent;
    const char *full_parent;
    int n;

    /* If parent dir is ignored, all children are ignored too. */
    gboolean ignored;
} IterCBData;

static int
add_dir_recursive (const char *path, const char *full_path, SeafStat *st,
                   AddParams *params, gboolean ignored);

static int
iter_dir_cb (wchar_t *full_parent_w,
             WIN32_FIND_DATAW *fdata,
             void *user_data,
             gboolean *stop)
{
    IterCBData *data = user_data;
    AddParams *params = data->add_params;
    AddOptions *options = params->options;
    char *dname = NULL, *path = NULL, *full_path = NULL;
    SeafStat st;
    int ret = 0;

    dname = g_utf16_to_utf8 (fdata->cFileName, -1, NULL, NULL, NULL);
    if (!dname) {
        goto out;
    }

    path = g_build_path ("/", data->parent, dname, NULL);
    full_path = g_build_path ("/", params->worktree, path, NULL);

    seaf_stat_from_find_data (fdata, &st);

    if (data->ignored ||
        should_ignore(data->full_parent, dname, params->ignore_list)) {
        if (options && options->startup_scan) {
            if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
                add_dir_recursive (path, full_path, &st, params, TRUE);
            else
                seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                      params->repo_id,
                                                      path,
                                                      S_IFREG,
                                                      SYNC_STATUS_IGNORED,
                                                      TRUE);
        }
        goto out;
    }

    if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        ret = add_dir_recursive (path, full_path, &st, params, FALSE);
    else
        ret = add_file (params->repo_id,
                        params->version,
                        params->modifier,
                        params->istate,
                        path,
                        full_path,
                        &st,
                        params->crypt,
                        params->total_size,
                        params->remain_files,
                        params->options);

    ++(data->n);

out:
    g_free (dname);
    g_free (path);
    g_free (full_path);

    return 0;
}

static int
add_dir_recursive (const char *path, const char *full_path, SeafStat *st,
                   AddParams *params, gboolean ignored)
{
    AddOptions *options = params->options;
    IterCBData data;
    wchar_t *full_path_w;
    int ret = 0;
    gboolean is_writable = TRUE;

    memset (&data, 0, sizeof(data));
    data.add_params = params;
    data.parent = path;
    data.full_parent = full_path;
    data.ignored = ignored;

    full_path_w = win32_long_path (full_path);
    ret = traverse_directory_win32 (full_path_w, iter_dir_cb, &data);
    g_free (full_path_w);

    /* Ignore traverse dir error. */
    if (ret < 0) {
        seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                              params->repo_id,
                                              path,
                                              S_IFDIR,
                                              SYNC_STATUS_ERROR,
                                              TRUE);
        // The directory under Documents may be protected by the system on Windows and cannot be accessed, so the indexing errors for these folders are skipped.
        char *parent_dir = g_path_get_dirname (path);
        char *dir_name = NULL;
        if (parent_dir) {
            dir_name = g_path_get_basename (parent_dir);
        }
        if (!ignored && g_strcmp0 (dir_name, "Documents") != 0) {
            send_file_sync_error_notification (params->repo_id, NULL, path,
                                               SYNC_ERROR_ID_INDEX_ERROR);
        }
        g_free (parent_dir);
        g_free (dir_name);
        return 0;
    }

    if (ignored) {
        seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                              params->repo_id,
                                              path,
                                              S_IFDIR,
                                              SYNC_STATUS_IGNORED,
                                              TRUE);
        return 0;
    }

    if (options)
        is_writable = is_path_writable(params->repo_id,
                                        options->is_repo_ro, path);

    /* Update active path status for empty dir */
    if (options && options->startup_scan && ret == 0) {
        SyncStatus status;
        struct cache_entry *ce = index_name_exists (params->istate, path,
                                                    strlen(path), 0);
        if (!ce)
            status = SYNC_STATUS_SYNCING;
        else
            status = SYNC_STATUS_SYNCED;


        if (status == SYNC_STATUS_SYNCED || is_writable)
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  params->repo_id,
                                                  path,
                                                  S_IFDIR,
                                                  status,
                                                  FALSE);
    }

    if (data.n == 0 && path[0] != 0 && !params->ignore_empty_dir && is_writable) {
        if (!params->remain_files || *(params->remain_files) == NULL) {
            int rc = add_empty_dir_to_index (params->istate, path, st);
            if (rc == 1 && options && options->changeset) {
                unsigned char allzero[20] = {0};
                add_to_changeset (options->changeset,
                                  DIFF_STATUS_DIR_ADDED,
                                  allzero,
                                  st,
                                  NULL,
                                  path,
                                  NULL);
            }
        } else
            g_queue_push_tail (*(params->remain_files), g_strdup(path));
    }

    return ret;
}

static int
add_recursive (const char *repo_id,
               int version,
               const char *modifier,
               struct index_state *istate, 
               const char *worktree,
               const char *path,
               SeafileCrypt *crypt,
               gboolean ignore_empty_dir,
               GList *ignore_list,
               gint64 *total_size,
               GQueue **remain_files,
               AddOptions *options)
{
    char *full_path;
    SeafStat st;
    int ret = 0;

    full_path = g_build_path (PATH_SEPERATOR, worktree, path, NULL);
    if (seaf_stat (full_path, &st) < 0) {
        seaf_warning ("Failed to stat %s.\n", full_path);
        g_free (full_path);
        seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                              repo_id,
                                              path,
                                              0,
                                              SYNC_STATUS_ERROR,
                                              TRUE);
        /* Ignore error */
        return 0;
    }

    if (S_ISREG(st.st_mode)) {
        ret = add_file (repo_id,
                        version,
                        modifier,
                        istate, 
                        path,
                        full_path,
                        &st,
                        crypt,
                        total_size,
                        remain_files,
                        options);
    } else if (S_ISDIR(st.st_mode)) {
        AddParams params = {
            .repo_id = repo_id,
            .version = version,
            .modifier = modifier,
            .istate = istate,
            .worktree = worktree,
            .crypt = crypt,
            .ignore_empty_dir = ignore_empty_dir,
            .ignore_list = ignore_list,
            .total_size = total_size,
            .remain_files = remain_files,
            .options = options,
        };

        ret = add_dir_recursive (path, full_path, &st, &params, FALSE);
    }

    g_free (full_path);
    return ret;
}

static gboolean
is_empty_dir (const char *path, GList *ignore_list)
{
    WIN32_FIND_DATAW fdata;
    HANDLE handle;
    wchar_t *pattern;
    wchar_t *path_w;
    char *dname;
    int path_len_w;
    DWORD error;
    gboolean ret = TRUE;

    path_w = win32_long_path (path);

    path_len_w = wcslen(path_w);

    pattern = g_new0 (wchar_t, (path_len_w + 3));
    wcscpy (pattern, path_w);
    wcscat (pattern, L"\\*");

    handle = FindFirstFileW (pattern, &fdata);
    if (handle == INVALID_HANDLE_VALUE) {
        seaf_warning ("FindFirstFile failed %s: %lu.\n",
                      path, GetLastError());
        ret = FALSE;
        goto out;
    }

    do {
        if (wcscmp (fdata.cFileName, L".") == 0 ||
            wcscmp (fdata.cFileName, L"..") == 0)
            continue;

        dname = g_utf16_to_utf8 (fdata.cFileName, -1, NULL, NULL, NULL);
        if (!dname || !should_ignore (path, dname, ignore_list)) {
            ret = FALSE;
            g_free (dname);
            FindClose (handle);
            goto out;
        }
        g_free (dname);
    } while (FindNextFileW (handle, &fdata) != 0);

    error = GetLastError();
    if (error != ERROR_NO_MORE_FILES) {
        seaf_warning ("FindNextFile failed %s: %lu.\n",
                      path, error);
    }

    FindClose (handle);

out:
    g_free (path_w);
    g_free (pattern);
    return ret;
}

#endif  /* WIN32 */

/* Returns whether the file should be removed from index. */
static gboolean
check_locked_file_before_remove (LockedFileSet *fset, const char *path)
{
#if defined WIN32 || defined __APPLE__
    if (!fset)
        return TRUE;

    LockedFile *file = locked_file_set_lookup (fset, path);
    gboolean ret = TRUE;

    if (file)
        ret = FALSE;

    return ret;
#else
    return TRUE;
#endif
}

static void
remove_deleted (struct index_state *istate, const char *worktree, const char *prefix,
                GList *ignore_list, LockedFileSet *fset,
                const char *repo_id, gboolean is_repo_ro,
                ChangeSet *changeset)
{
    struct cache_entry **ce_array = istate->cache;
    struct cache_entry *ce;
    char path[SEAF_PATH_MAX];
    unsigned int i;
    SeafStat st;
    int ret;
    gboolean not_exist;

    char *full_prefix = g_strconcat (prefix, "/", NULL);
    int len = strlen(full_prefix);

    for (i = 0; i < istate->cache_nr; ++i) {
        ce = ce_array[i];

        if (!is_path_writable (repo_id, is_repo_ro, ce->name))
            continue;

        if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                                  repo_id, ce->name)) {
            seaf_debug ("Remove deleted: %s is locked on server, ignore.\n", ce->name);
            continue;
        }

        if (prefix[0] != 0 && strcmp (ce->name, prefix) != 0 &&
            strncmp (ce->name, full_prefix, len) != 0)
            continue;

        snprintf (path, SEAF_PATH_MAX, "%s/%s", worktree, ce->name);
        not_exist = FALSE;
        ret = seaf_stat (path, &st);
        if (ret < 0 && errno == ENOENT)
            not_exist = TRUE;

        if (S_ISDIR (ce->ce_mode)) {
            if (ce->ce_ctime.sec != 0 || ce_stage(ce) != 0) {
                if (not_exist || (ret == 0 && !S_ISDIR (st.st_mode))) {
                    /* Add to changeset only if dir is removed. */
                    ce->ce_flags |= CE_REMOVE;
                    if (changeset)
                        /* Remove the parent dir from change set if it becomes
                         * empty. If in the work tree the empty dir still exist,
                         * we'll add it back to changeset in add_recursive() later.
                         */
                        remove_from_changeset (changeset,
                                               DIFF_STATUS_DIR_DELETED,
                                               ce->name,
                                               TRUE,
                                               prefix);
                } else if (!is_empty_dir (path, ignore_list)) {
                    /* Don't add to changeset if empty dir became non-empty. */
                    ce->ce_flags |= CE_REMOVE;
                }
            }
        } else {
            /* If ce->ctime is 0 and stage is 0, it was not successfully checked out.
             * In this case we don't want to mistakenly remove the file
             * from the repo.
             */
            if ((not_exist || (ret == 0 && !S_ISREG (st.st_mode))) &&
                (ce->ce_ctime.sec != 0 || ce_stage(ce) != 0) &&
                check_locked_file_before_remove (fset, ce->name))
            {
                ce_array[i]->ce_flags |= CE_REMOVE;
                if (changeset)
                    remove_from_changeset (changeset,
                                           DIFF_STATUS_DELETED,
                                           ce->name,
                                           TRUE,
                                           prefix);
            }
        }
    }

    remove_marked_cache_entries (istate);

    g_free (full_prefix);
}

static int
scan_worktree_for_changes (struct index_state *istate, SeafRepo *repo,
                           SeafileCrypt *crypt, GList *ignore_list,
                           LockedFileSet *fset)
{
    remove_deleted (istate, repo->worktree, "", ignore_list, fset,
                    repo->id, repo->is_readonly, repo->changeset);

    AddOptions options;
    memset (&options, 0, sizeof(options));
    options.fset = fset;
    options.is_repo_ro = repo->is_readonly;
    options.changeset = repo->changeset;

    if (add_recursive (repo->id, repo->version, repo->email,
                       istate, repo->worktree, "", crypt, FALSE, ignore_list,
                       NULL, NULL, &options) < 0)
        return -1;

    return 0;
}

static gboolean
check_full_path_ignore (const char *worktree, const char *path, GList *ignore_list)
{
    char **tokens;
    guint i;
    guint n;
    gboolean ret = FALSE;

    tokens = g_strsplit (path, "/", 0);
    n = g_strv_length (tokens);
    for (i = 0; i < n; ++i) {
        /* don't check ignore_list */
        if (should_ignore (NULL, tokens[i], ignore_list)) {
            ret = TRUE;
            goto out;
        }
    }

    char *full_path = g_build_path ("/", worktree, path, NULL);
    if (seaf_repo_check_ignore_file (ignore_list, full_path))
        ret = TRUE;
    g_free (full_path);

out:
    g_strfreev (tokens);
    return ret;
}

static int
add_path_to_index (SeafRepo *repo, struct index_state *istate,
                   SeafileCrypt *crypt, const char *path, GList *ignore_list,
                   GList **scanned_dirs, gint64 *total_size, GQueue **remain_files,
                   LockedFileSet *fset)
{
    char *full_path;
    SeafStat st;
    AddOptions options;

    /* When a repo is initially added, a SCAN_DIR event will be created
     * for the worktree root "".
     */
    if (path[0] == 0) {
        remove_deleted (istate, repo->worktree, "", ignore_list, fset,
                        repo->id, repo->is_readonly, repo->changeset);

        memset (&options, 0, sizeof(options));
        options.fset = fset;
        options.is_repo_ro = repo->is_readonly;
        options.startup_scan = TRUE;
        options.changeset = repo->changeset;

        add_recursive (repo->id, repo->version, repo->email, istate,
                       repo->worktree, path,
                       crypt, FALSE, ignore_list,
                       total_size, remain_files, &options);

        return 0;
    }

    /* If we've recursively scanned the parent directory, don't need to scan
     * any files under it any more.
     */
    GList *ptr;
    char *dir, *full_dir;
    for (ptr = *scanned_dirs; ptr; ptr = ptr->next) {
        dir = ptr->data;
        /* exact match */
        if (strcmp (dir, path) == 0) {
            seaf_debug ("%s has been scanned before, skip adding.\n", path);
            return 0;
        }

        /* prefix match. */
        full_dir = g_strconcat (dir, "/", NULL);
        if (strncmp (full_dir, path, strlen(full_dir)) == 0) {
            g_free (full_dir);
            seaf_debug ("%s has been scanned before, skip adding.\n", path);
            return 0;
        }
        g_free (full_dir);
    }

    if (check_full_path_ignore (repo->worktree, path, ignore_list))
        return 0;

    full_path = g_build_filename (repo->worktree, path, NULL);

    if (seaf_stat (full_path, &st) < 0) {
        if (errno != ENOENT)
            send_file_sync_error_notification (repo->id, repo->name, path,
                                               SYNC_ERROR_ID_INDEX_ERROR);
        seaf_warning ("Failed to stat %s: %s.\n", path, strerror(errno));
        g_free (full_path);
        return -1;
    }

    if (S_ISDIR(st.st_mode))
        *scanned_dirs = g_list_prepend (*scanned_dirs, g_strdup(path));

    memset (&options, 0, sizeof(options));
    options.fset = fset;
    options.is_repo_ro = repo->is_readonly;
    options.changeset = repo->changeset;

    /* Add is always recursive */
    add_recursive (repo->id, repo->version, repo->email, istate, repo->worktree, path,
                   crypt, FALSE, ignore_list, total_size, remain_files, &options);

    g_free (full_path);
    return 0;
}

#if 0

static int
add_path_to_index (SeafRepo *repo, struct index_state *istate,
                   SeafileCrypt *crypt, const char *path, GList *ignore_list,
                   GList **scanned_dirs, gint64 *total_size, GQueue **remain_files,
                   LockedFileSet *fset)
{
    /* If we've recursively scanned the parent directory, don't need to scan
     * any files under it any more.
     */
    GList *ptr;
    char *dir, *full_dir;
    for (ptr = *scanned_dirs; ptr; ptr = ptr->next) {
        dir = ptr->data;

        /* Have scanned from root directory. */
        if (dir[0] == 0) {
            seaf_debug ("%s has been scanned before, skip adding.\n", path);
            return 0;
        }

        /* exact match */
        if (strcmp (dir, path) == 0) {
            seaf_debug ("%s has been scanned before, skip adding.\n", path);
            return 0;
        }

        /* prefix match. */
        full_dir = g_strconcat (dir, "/", NULL);
        if (strncmp (full_dir, path, strlen(full_dir)) == 0) {
            g_free (full_dir);
            seaf_debug ("%s has been scanned before, skip adding.\n", path);
            return 0;
        }
        g_free (full_dir);
    }

    if (path[0] != 0 && check_full_path_ignore (repo->worktree, path, ignore_list))
        return 0;

    remove_deleted (istate, repo->worktree, path, ignore_list, NULL,
                    repo->id, repo->is_readonly, repo->changeset);

    *scanned_dirs = g_list_prepend (*scanned_dirs, g_strdup(path));

    AddOptions options;
    memset (&options, 0, sizeof(options));
    options.fset = fset;
    options.is_repo_ro = repo->is_readonly;
    options.changeset = repo->changeset;
    /* When something is changed in the root directory, update active path
     * sync status when scanning the worktree. This is inaccurate. This will
     * be changed after we process fs events on Mac more precisely.
     */
    if (path[0] == 0)
        options.startup_scan = TRUE;

    /* Add is always recursive */
    add_recursive (repo->id, repo->version, repo->email, istate, repo->worktree, path,
                   crypt, FALSE, ignore_list, total_size, remain_files, &options);

    return 0;
}

#endif  /* __APPLE__ */

static int
add_remain_files (SeafRepo *repo, struct index_state *istate,
                  SeafileCrypt *crypt, GQueue *remain_files,
                  GList *ignore_list, gint64 *total_size)
{
    char *path;
    char *full_path;
    SeafStat st;
    struct cache_entry *ce;

    while ((path = g_queue_pop_head (remain_files)) != NULL) {
        full_path = g_build_filename (repo->worktree, path, NULL);
        if (seaf_stat (full_path, &st) < 0) {
            seaf_warning ("Failed to stat %s: %s.\n", full_path, strerror(errno));
            g_free (path);
            g_free (full_path);
            continue;
        }

#ifndef WIN32
    char *base_name = g_path_get_basename(full_path);
        if (!seaf->hide_windows_incompatible_path_notification &&
            check_path_ignore_on_windows (base_name)) {

            send_file_sync_error_notification (repo->id, repo->name, path,
                                               SYNC_ERROR_ID_INVALID_PATH_ON_WINDOWS);
        }
    g_free (base_name);
#endif

        if (S_ISREG(st.st_mode)) {
            gboolean added = FALSE;
            int ret = 0;
            ret = add_to_index (repo->id, repo->version, istate, path, full_path,
                                &st, 0, crypt, index_cb, repo->email, &added);
            if (added) {
                ce = index_name_exists (istate, path, strlen(path), 0);
                add_to_changeset (repo->changeset,
                                  DIFF_STATUS_ADDED,
                                  ce->sha1,
                                  &st,
                                  repo->email,
                                  path,
                                  NULL);

                *total_size += (gint64)(st.st_size);
                if (*total_size >= MAX_COMMIT_SIZE) {
                    g_free (path);
                    g_free (full_path);
                    break;
                }
            } else {
                seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                      repo->id,
                                                      path,
                                                      S_IFREG,
                                                      SYNC_STATUS_SYNCED,
                                                      TRUE);
            }
            if (ret < 0) {
                seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                      repo->id,
                                                      path,
                                                      S_IFREG,
                                                      SYNC_STATUS_ERROR,
                                                      TRUE);
                if (seaf_util_exists (full_path)) {
                    send_file_sync_error_notification (repo->id, NULL, path,
                                                       SYNC_ERROR_ID_INDEX_ERROR);
                }
            }
        } else if (S_ISDIR(st.st_mode)) {
            if (is_empty_dir (full_path, ignore_list)) {
                int rc = add_empty_dir_to_index (istate, path, &st);
                if (rc == 1) {
                    unsigned char allzero[20] = {0};
                    add_to_changeset (repo->changeset,
                                      DIFF_STATUS_DIR_ADDED,
                                      allzero,
                                      &st,
                                      NULL,
                                      path,
                                      NULL);
                }
            }
        }
        g_free (path);
        g_free (full_path);
    }

    return 0;
}

static void
try_add_empty_parent_dir_entry (const char *worktree,
                                struct index_state *istate,
                                const char *path)
{
    if (index_name_exists (istate, path, strlen(path), 0) != NULL)
        return;

    char *parent_dir = g_path_get_dirname (path);

    /* Parent dir is the worktree dir. */
    if (strcmp (parent_dir, ".") == 0) {
        g_free (parent_dir);
        return;
    }

    char *full_dir = g_build_filename (worktree, parent_dir, NULL);
    SeafStat st;
    if (seaf_stat (full_dir, &st) < 0) {
        goto out;
    }

    add_empty_dir_to_index_with_check (istate, parent_dir, &st);

out:
    g_free (parent_dir);
    g_free (full_dir);
}

static void
try_add_empty_parent_dir_entry_from_wt (const char *worktree,
                                        struct index_state *istate,
                                        GList *ignore_list,
                                        const char *path)
{
    if (index_name_exists (istate, path, strlen(path), 0) != NULL)
        return;

    char *parent_dir = g_path_get_dirname (path);

    /* Parent dir is the worktree dir. */
    if (strcmp (parent_dir, ".") == 0) {
        g_free (parent_dir);
        return;
    }

    char *full_dir = g_build_filename (worktree, parent_dir, NULL);
    SeafStat st;
    if (seaf_stat (full_dir, &st) < 0) {
        goto out;
    }

    if (is_empty_dir (full_dir, ignore_list)) {
#ifdef WIN32
        wchar_t *parent_dir_w = g_utf8_to_utf16 (parent_dir, -1, NULL, NULL, NULL);
        wchar_t *pw;
        for (pw = parent_dir_w; *pw != L'\0'; ++pw)
            if (*pw == L'/')
                *pw = L'\\';

        wchar_t *long_path = win32_83_path_to_long_path (worktree,
                                                         parent_dir_w,
                                                         wcslen(parent_dir_w));
        g_free (parent_dir_w);
        if (!long_path) {
            seaf_warning ("Convert %s to long path failed.\n", parent_dir);
            goto out;
        }

        char *utf8_path = g_utf16_to_utf8 (long_path, -1, NULL, NULL, NULL);
        if (!utf8_path) {
            g_free (long_path);
            goto out;
        }

        char *p;
        for (p = utf8_path; *p != 0; ++p)
            if (*p == '\\')
                *p = '/';
        g_free (long_path);

        add_empty_dir_to_index (istate, utf8_path, &st);
#else
        add_empty_dir_to_index (istate, parent_dir, &st);
#endif
    }

out:
    g_free (parent_dir);
    g_free (full_dir);
}

static void
update_attributes (SeafRepo *repo,
                   struct index_state *istate,
                   const char *worktree,
                   const char *path)
{
    ChangeSet *changeset = repo->changeset;
    char *full_path;
    struct cache_entry *ce;
    SeafStat st;

    ce = index_name_exists (istate, path, strlen(path), 0);
    if (!ce)
        return;

    full_path = g_build_filename (worktree, path, NULL);
    if (seaf_stat (full_path, &st) < 0) {
        seaf_warning ("Failed to stat %s: %s.\n", full_path, strerror(errno));
        g_free (full_path);
        return;
    }

    unsigned int new_mode = create_ce_mode (st.st_mode);
    if (new_mode != ce->ce_mode || st.st_mtime != ce->ce_mtime.sec) {
        ce->ce_mode = new_mode;
        ce->ce_mtime.sec = st.st_mtime;
        istate->cache_changed = 1;
        add_to_changeset (changeset,
                          DIFF_STATUS_MODIFIED,
                          ce->sha1,
                          &st,
                          repo->email,
                          path,
                          NULL);
    }
    g_free (full_path);
}

#ifdef WIN32
static void
scan_subtree_for_deletion (const char *repo_id,
                           struct index_state *istate,
                           const char *worktree,
                           const char *path,
                           GList *ignore_list,
                           LockedFileSet *fset,
                           gboolean is_readonly,
                           GList **scanned_dirs,
                           ChangeSet *changeset)
{
    wchar_t *path_w = NULL;
    wchar_t *dir_w = NULL;
    wchar_t *p;
    char *dir = NULL;
    char *p2;

    /* In most file systems, like NTFS, 8.3 format path should contain ~.
     * Also note that *~ files are ignored.
     */
    if (!strchr (path, '~') || path[strlen(path)-1] == '~')
        return;

    path_w = g_utf8_to_utf16 (path, -1, NULL, NULL, NULL);

    for (p = path_w; *p != L'\0'; ++p)
        if (*p == L'/')
            *p = L'\\';

    while (1) {
        p = wcsrchr (path_w, L'\\');
        if (p)
            *p = L'\0';
        else
            break;

        dir_w = win32_83_path_to_long_path (worktree, path_w, wcslen(path_w));
        if (dir_w)
            break;
    }

    if (!dir_w)
        dir_w = wcsdup(L"");

    dir = g_utf16_to_utf8 (dir_w, -1, NULL, NULL, NULL);
    if (!dir)
        goto out;

    for (p2 = dir; *p2 != 0; ++p2)
        if (*p2 == '\\')
            *p2 = '/';

    /* If we've recursively scanned the parent directory, don't need to scan
     * any files under it any more.
     */
    GList *ptr;
    char *s, *full_s;
    for (ptr = *scanned_dirs; ptr; ptr = ptr->next) {
        s = ptr->data;

        /* Have scanned from root directory. */
        if (s[0] == 0) {
            goto out;
        }

        /* exact match */
        if (strcmp (s, path) == 0) {
            goto out;
        }

        /* prefix match. */
        full_s = g_strconcat (s, "/", NULL);
        if (strncmp (full_s, dir, strlen(full_s)) == 0) {
            g_free (full_s);
            goto out;
        }
        g_free (full_s);
    }

    *scanned_dirs = g_list_prepend (*scanned_dirs, g_strdup(dir));

    remove_deleted (istate, worktree, dir, ignore_list, fset,
                    repo_id, is_readonly, changeset);

    /* After remove_deleted(), empty dirs are left not removed in changeset.
     * This can be fixed by removing the accurate deleted path. In most cases,
     * basename doesn't contain ~, so we can always get the accurate path.
     */
    /* if (!convertion_failed) { */
    /*     char *basename = strrchr (path, '/'); */
    /*     char *deleted_path = NULL; */
    /*     if (basename) { */
    /*         deleted_path = g_build_path ("/", dir, basename, NULL); */
    /*         add_to_changeset (changeset, */
    /*                           DIFF_STATUS_DELETED, */
    /*                           NULL, */
    /*                           NULL, */
    /*                           NULL, */
    /*                           deleted_path, */
    /*                           NULL, */
    /*                           FALSE); */
    /*         g_free (deleted_path); */
    /*     } */
    /* } */

out:
    g_free (path_w);
    g_free (dir_w);
    g_free (dir);
}
#else
static void
scan_subtree_for_deletion (const char *repo_id,
                           struct index_state *istate,
                           const char *worktree,
                           const char *path,
                           GList *ignore_list,
                           LockedFileSet *fset,
                           gboolean is_readonly,
                           GList **scanned_dirs,
                           ChangeSet *changeset)
{
}
#endif

/* Return TRUE if the caller should stop processing next event. */
static gboolean
handle_add_files (SeafRepo *repo, struct index_state *istate,
                  SeafileCrypt *crypt, GList *ignore_list,
                  LockedFileSet *fset,
                  WTStatus *status, WTEvent *event,
                  GList **scanned_dirs, gint64 *total_size)
{
    SyncInfo *info;

    if (!repo->create_partial_commit) {
        /* XXX: We now use remain_files = NULL to signify not creating
         * partial commits. It's better to use total_size = NULL for
         * that purpose.
         */
        add_path_to_index (repo, istate, crypt, event->path,
                           ignore_list, scanned_dirs,
                           total_size, NULL, NULL);
    } else if (!event->remain_files) {
        GQueue *remain_files = NULL;
        add_path_to_index (repo, istate, crypt, event->path,
                           ignore_list, scanned_dirs,
                           total_size, &remain_files, fset);
        if (*total_size >= MAX_COMMIT_SIZE) {
            seaf_message ("Creating partial commit after adding %s.\n",
                          event->path);

            status->partial_commit = TRUE;

            /* An event for a new folder may contain many files.
             * If the total_size become larger than 100MB after adding
             * some of these files, the remaining file paths will be
             * cached in remain files. This way we don't need to scan
             * the folder again next time.
             */
            if (remain_files) {
                if (g_queue_get_length (remain_files) == 0) {
                    g_queue_free (remain_files);
                    return TRUE;
                }

                seaf_message ("Remain files for %s.\n", event->path);

                /* Cache remaining files in the event structure. */
                event->remain_files = remain_files;

                pthread_mutex_lock (&status->q_lock);
                g_queue_push_head (status->event_q, event);
                pthread_mutex_unlock (&status->q_lock);

                info = seaf_sync_manager_get_sync_info (seaf->sync_mgr, repo->id);
                if (!info->multipart_upload) {
                    info->multipart_upload = TRUE;
                    info->total_bytes = *total_size;
                }
            }

            return TRUE;
        }
    } else {
        seaf_message ("Adding remaining files for %s.\n", event->path);

        add_remain_files (repo, istate, crypt, event->remain_files,
                          ignore_list, total_size);
        if (g_queue_get_length (event->remain_files) != 0) {
            pthread_mutex_lock (&status->q_lock);
            g_queue_push_head (status->event_q, event);
            pthread_mutex_unlock (&status->q_lock);
            return TRUE;
        } else {
            info = seaf_sync_manager_get_sync_info (seaf->sync_mgr, repo->id);
            info->end_multipart_upload = TRUE;
            return TRUE;
        }
        if (*total_size >= MAX_COMMIT_SIZE)
            return TRUE;
    }

    return FALSE;
}

#ifdef __APPLE__

/* struct _WTDirent { */
/*     char *dname; */
/*     struct stat st; */
/* }; */
/* typedef struct _WTDirent WTDirent; */

/* static gint */
/* compare_wt_dirents (gconstpointer a, gconstpointer b) */
/* { */
/*     const WTDirent *dent_a = a, *dent_b = b; */

/*     return (strcmp (dent_a->dname, dent_b->dname)); */
/* } */

/* static GList * */
/* get_sorted_wt_dirents (const char *dir_path, const char *full_dir_path, */
/*                        gboolean *error) */
/* { */
/*     GDir *dir; */
/*     GError *err = NULL; */
/*     const char *name; */
/*     char *dname; */
/*     char *full_sub_path, *sub_path; */
/*     WTDirent *dent; */
/*     GList *ret = NULL; */

/*     dir = g_dir_open (full_dir_path, 0, &err); */
/*     if (!dir) { */
/*         seaf_warning ("Failed to open dir %s: %s.\n", full_dir_path, err->message); */
/*         *error = TRUE; */
/*         return NULL; */
/*     } */

/*     while ((name = g_dir_read_name(dir)) != NULL) { */
/*         dname = g_utf8_normalize (name, -1, G_NORMALIZE_NFC); */
/*         sub_path = g_strconcat (dir_path, "/", dname, NULL); */
/*         full_sub_path = g_strconcat (full_dir_path, "/", dname, NULL); */

/*         dent = g_new0 (WTDirent, 1); */
/*         dent->dname = dname; */

/*         if (stat (full_sub_path, &dent->st) < 0) { */
/*             seaf_warning ("Failed to stat %s: %s.\n", full_sub_path, strerror(errno)); */
/*             g_free (dname); */
/*             g_free (sub_path); */
/*             g_free (full_sub_path); */
/*             g_free (dent); */
/*             continue; */
/*         } */

/*         ret = g_list_prepend (ret, dent); */

/*         g_free (sub_path); */
/*         g_free (full_sub_path); */
/*     } */

/*     g_dir_close (dir); */

/*     ret = g_list_sort (ret, compare_wt_dirents); */
/*     return ret; */
/* } */

/* static void */
/* wt_dirent_free (WTDirent *dent) */
/* { */
/*     if (!dent) */
/*         return; */
/*     g_free (dent->dname); */
/*     g_free (dent); */
/* } */

/* inline static char * */
/* concat_sub_path (const char *dir, const char *dname) */
/* { */
/*     if (dir[0] != 0) */
/*         return g_strconcat(dir, "/", dname, NULL); */
/*     else */
/*         return g_strdup(dname); */
/* } */

/* static int */
/* get_changed_paths_in_folder (SeafRepo *repo, struct index_state *istate, */
/*                              const char *dir_path, */
/*                              GList **add, GList **mod, GList **del) */
/* { */
/*     char *full_dir_path; */
/*     GList *wt_dents = NULL, *index_dents = NULL; */
/*     gboolean error = FALSE; */

/*     full_dir_path = g_build_filename(repo->worktree, dir_path, NULL); */

/*     wt_dents = get_sorted_wt_dirents (dir_path, full_dir_path, &error); */
/*     if (error) { */
/*         g_free (full_dir_path); */
/*         return -1; */
/*     } */

/*     index_dents = list_dirents_from_index (istate, dir_path); */

/*     GList *p; */
/*     IndexDirent *dent; */
/*     for (p = index_dents; p; p = p->next) { */
/*         dent = p->data; */
/*     } */

/*     GList *p1 = wt_dents, *p2 = index_dents; */
/*     WTDirent *dent1; */
/*     IndexDirent *dent2; */

/*     while (p1 && p2) { */
/*         dent1 = p1->data; */
/*         dent2 = p2->data; */

/*         int rc = strcmp (dent1->dname, dent2->dname); */
/*         if (rc == 0) { */
/*             if (S_ISREG(dent1->st.st_mode) && !dent2->is_dir) { */
/*                 if (dent1->st.st_mtime != dent2->ce->ce_mtime.sec) */
/*                     *mod = g_list_prepend (*mod, concat_sub_path(dir_path, dent1->dname)); */
/*             } else if ((S_ISREG(dent1->st.st_mode) && dent2->is_dir) || */
/*                        (S_ISDIR(dent1->st.st_mode) && !dent2->is_dir)) { */
/*                 *add = g_list_prepend (*add, concat_sub_path(dir_path, dent1->dname)); */
/*                 *del = g_list_prepend (*del, concat_sub_path(dir_path, dent1->dname)); */
/*             } */
/*             p1 = p1->next; */
/*             p2 = p2->next; */
/*         } else if (rc < 0) { */
/*             *add = g_list_prepend (*add, concat_sub_path(dir_path, dent1->dname)); */
/*             p1 = p1->next; */
/*         } else { */
/*             *del = g_list_prepend (*del, concat_sub_path(dir_path, dent2->dname)); */
/*             p2 = p2->next; */
/*         } */
/*     } */

/*     while (p1) { */
/*         dent1 = p1->data; */
/*         *add = g_list_prepend (*add, concat_sub_path(dir_path, dent1->dname)); */
/*         p1 = p1->next; */
/*     } */

/*     while (p2) { */
/*         dent2 = p2->data; */
/*         *del = g_list_prepend (*del, concat_sub_path(dir_path, dent2->dname)); */
/*         p2 = p2->next; */
/*     } */

/*     g_free (full_dir_path); */
/*     g_list_free_full (wt_dents, (GDestroyNotify)wt_dirent_free); */
/*     g_list_free_full (index_dents, (GDestroyNotify)index_dirent_free); */
/*     return 0; */
/* } */

#endif  /* __APPLE__ */

static void
update_active_file (SeafRepo *repo,
                    const char *path,
                    SeafStat *st,
                    struct index_state *istate,
                    gboolean ignored)
{
    if (ignored) {
        seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                              repo->id,
                                              path,
                                              S_IFREG,
                                              SYNC_STATUS_IGNORED,
                                              TRUE);
    } else {
        SyncStatus status;
        gboolean is_writable;

        struct cache_entry *ce = index_name_exists(istate, path, strlen(path), 0);
        if (!ce || ie_match_stat(ce, st, 0) != 0)
            status = SYNC_STATUS_SYNCING;
        else
            status = SYNC_STATUS_SYNCED;

        is_writable = is_path_writable (repo->id, repo->is_readonly, path);

        if (!is_writable && status == SYNC_STATUS_SYNCING)
            seaf_sync_manager_delete_active_path (seaf->sync_mgr,
                                                  repo->id,
                                                  path);
        else
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  repo->id,
                                                  path,
                                                  S_IFREG,
                                                  status,
                                                  FALSE);
    }
}

#ifdef WIN32

typedef struct _UpdatePathData {
    SeafRepo *repo;
    struct index_state *istate;
    GList *ignore_list;

    const char *parent;
    const char *full_parent;
    gboolean ignored;
} UpdatePathData;

static void
update_active_path_recursive (SeafRepo *repo,
                              const char *path,
                              struct index_state *istate,
                              GList *ignore_list,
                              gboolean ignored);

static int
update_active_path_cb (wchar_t *full_parent_w,
                       WIN32_FIND_DATAW *fdata,
                       void *user_data,
                       gboolean *stop)
{
    UpdatePathData *upd_data = user_data;
    char *dname;
    char *path;
    gboolean ignored = FALSE;
    SeafStat st;

    dname = g_utf16_to_utf8 (fdata->cFileName, -1, NULL, NULL, NULL);
    if (!dname)
        return 0;

    path = g_build_path ("/", upd_data->parent, dname, NULL);

    if (upd_data->ignored || should_ignore (upd_data->full_parent, dname, upd_data->ignore_list))
        ignored = TRUE;

    seaf_stat_from_find_data (fdata, &st);

    if (fdata->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
        update_active_path_recursive (upd_data->repo,
                                      path,
                                      upd_data->istate,
                                      upd_data->ignore_list,
                                      ignored);
    } else {
        update_active_file (upd_data->repo,
                            path,
                            &st,
                            upd_data->istate,
                            ignored);
    }

    g_free (dname);
    g_free (path);

    return 0;
}

static void
update_active_path_recursive (SeafRepo *repo,
                              const char *path,
                              struct index_state *istate,
                              GList *ignore_list,
                              gboolean ignored)
{
    char *full_path;
    wchar_t *full_path_w;
    int ret = 0;
    UpdatePathData upd_data;

    full_path = g_build_filename (repo->worktree, path, NULL);

    memset (&upd_data, 0, sizeof(upd_data));
    upd_data.repo = repo;
    upd_data.istate = istate;
    upd_data.ignore_list = ignore_list;
    upd_data.parent = path;
    upd_data.full_parent = full_path;
    upd_data.ignored = ignored;

    full_path_w = win32_long_path (full_path);
    ret = traverse_directory_win32 (full_path_w, update_active_path_cb, &upd_data);
    g_free (full_path_w);
    g_free (full_path);

    if (ret < 0)
        return;

    /* Don't set sync status for read-only paths, since changes to read-only
     * files are ignored.
     */
    if (!is_path_writable (repo->id, repo->is_readonly, path))
        return;

    /* traverse_directory_win32() returns number of entries in the directory. */
    if (ret == 0 && path[0] != 0) {
        if (ignored) {
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  repo->id,
                                                  path,
                                                  S_IFDIR,
                                                  SYNC_STATUS_IGNORED,
                                                  FALSE);
        } else {
            /* There is no need to update an empty dir. */
            SyncStatus status;
            struct cache_entry *ce = index_name_exists(istate, path, strlen(path), 0);
            if (!ce)
                status = SYNC_STATUS_SYNCING;
            else
                status = SYNC_STATUS_SYNCED;
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  repo->id,
                                                  path,
                                                  S_IFDIR,
                                                  status,
                                                  FALSE);
        }
    }
}

#else

static void
update_active_path_recursive (SeafRepo *repo,
                              const char *path,
                              struct index_state *istate,
                              GList *ignore_list,
                              gboolean ignored)
{
    GDir *dir;
    GError *error = NULL;
    const char *name;
    char *dname;
    char *full_path, *full_sub_path, *sub_path;
    struct stat st;
    gboolean ignore_sub;

    full_path = g_build_filename(repo->worktree, path, NULL);

    dir = g_dir_open (full_path, 0, &error);
    if (!dir) {
        seaf_warning ("Failed to open dir %s: %s.\n", full_path, error->message);
        g_free (full_path);
        return;
    }

    int n = 0;
    while ((name = g_dir_read_name(dir)) != NULL) {
        ++n;

        dname = g_utf8_normalize (name, -1, G_NORMALIZE_NFC);
        sub_path = g_strconcat (path, "/", dname, NULL);
        full_sub_path = g_strconcat (full_path, "/", dname, NULL);

        ignore_sub = FALSE;
        if (ignored || should_ignore(full_path, dname, ignore_list))
            ignore_sub = TRUE;

        if (seaf->ignore_symlinks && is_symlink(full_sub_path)) {
            g_free (dname);
            g_free (sub_path);
            g_free (full_sub_path);
            continue;
        }

        if (stat (full_sub_path, &st) < 0) {
            seaf_warning ("Failed to stat %s: %s.\n", full_sub_path, strerror(errno));
            g_free (dname);
            g_free (sub_path);
            g_free (full_sub_path);
            continue;
        }

        if (S_ISDIR(st.st_mode)) {
            update_active_path_recursive (repo, sub_path, istate, ignore_list,
                                          ignore_sub);
        } else if (S_ISREG(st.st_mode)) {
            update_active_file (repo, sub_path, &st, istate,
                                ignore_sub);
        }

        g_free (dname);
        g_free (sub_path);
        g_free (full_sub_path);
    }

    g_dir_close (dir);

    g_free (full_path);

    /* Don't set sync status for read-only paths, since changes to read-only
     * files are ignored.
     */
    if (!is_path_writable (repo->id, repo->is_readonly, path))
        return;

    if (n == 0 && path[0] != 0) {
        if (ignored) {
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  repo->id,
                                                  path,
                                                  S_IFDIR,
                                                  SYNC_STATUS_IGNORED,
                                                  TRUE);
        } else {
            /* There is no need to update an empty dir. */
            SyncStatus status;
            struct cache_entry *ce = index_name_exists(istate, path, strlen(path), 0);
            if (!ce)
                status = SYNC_STATUS_SYNCING;
            else
                status = SYNC_STATUS_SYNCED;
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  repo->id,
                                                  path,
                                                  S_IFDIR,
                                                  status,
                                                  TRUE);
        }
    }
}

#endif  /* WIN32 */

static void
process_active_path (SeafRepo *repo, const char *path,
                     struct index_state *istate, GList *ignore_list)
{
    SeafStat st;
    gboolean ignored = FALSE;

    char *fullpath = g_build_filename (repo->worktree, path, NULL);
    if (seaf_stat (fullpath, &st) < 0) {
        g_free (fullpath);
        return;
    }

    if (check_full_path_ignore (repo->worktree, path, ignore_list))
        ignored = TRUE;

    if (S_ISREG(st.st_mode)) {
        if (!seaf_filelock_manager_is_file_locked(seaf->filelock_mgr,
                                                  repo->id, path)) {
            update_active_file (repo, path, &st, istate, ignored);
        }
    } else {
        update_active_path_recursive (repo, path, istate, ignore_list, ignored);
    }

    g_free (fullpath);
}

#ifdef __APPLE__

/* static void */
/* process_active_folder (SeafRepo *repo, const char *dir, */
/*                        struct index_state *istate, GList *ignore_list) */
/* { */
/*     GList *add = NULL, *mod = NULL, *del = NULL; */
/*     GList *p; */
/*     char *path; */

/*     /\* Delete event will be triggered on the deleted dir too. *\/ */
/*     if (!g_file_test (dir, G_FILE_TEST_IS_DIR)) */
/*         return; */

/*     if (get_changed_paths_in_folder (repo, istate, dir, &add, &mod, &del) < 0) { */
/*         seaf_warning ("Failed to get changed paths under %s.\n", dir); */
/*         return; */
/*     } */

/*     for (p = add; p; p = p->next) { */
/*         path = p->data; */
/*         process_active_path (repo, path, istate, ignore_list); */
/*     } */

/*     for (p = mod; p; p = p->next) { */
/*         path = p->data; */
/*         process_active_path (repo, path, istate, ignore_list); */
/*     } */

/*     g_list_free_full (add, g_free); */
/*     g_list_free_full (mod, g_free); */
/*     g_list_free_full (del, g_free); */
/* } */

#endif  /* __APPLE__ */

static void
update_path_sync_status (SeafRepo *repo, WTStatus *status,
                         struct index_state *istate, GList *ignore_list)
{
    char *path;

    while (1) {
        pthread_mutex_lock (&status->ap_q_lock);
        path = g_queue_pop_head (status->active_paths);
        pthread_mutex_unlock (&status->ap_q_lock);

        if (!path)
            break;

/* #ifdef __APPLE__ */
/*         process_active_folder (repo, path, istate, ignore_list); */
/* #else */
        process_active_path (repo, path, istate, ignore_list);
/* #endif */

        g_free (path);
    }
}

/* Excel first writes update to a temporary file and then rename the file to
 * xlsx. Unfortunately the temp file dosen't have specific pattern.
 * We can only ignore renaming from non xlsx file to xlsx file.
 */
static gboolean
ignore_xlsx_update (const char *src_path, const char *dst_path)
{
    GPatternSpec *pattern = g_pattern_spec_new ("*.xlsx");
    int ret = FALSE;

    if (!g_pattern_match_string(pattern, src_path) &&
        g_pattern_match_string(pattern, dst_path))
        ret = TRUE;

    g_pattern_spec_free (pattern);
    return ret;
}

static gboolean
is_seafile_backup_file (const char *path)
{
    GPatternSpec *pattern = g_pattern_spec_new ("*.sbak");
    int ret = FALSE;

    if (g_pattern_match_string(pattern, path))
        ret = TRUE;

    g_pattern_spec_free (pattern);
    return ret;
}

static void
handle_rename (SeafRepo *repo, struct index_state *istate,
               SeafileCrypt *crypt, GList *ignore_list,
               LockedFileSet *fset,
               WTEvent *event, GList **scanned_del_dirs,
               gint64 *total_size)
{
    char *fullpath = NULL;
    gboolean not_found, src_ignored, dst_ignored;

    seaf_sync_manager_delete_active_path (seaf->sync_mgr, repo->id, event->path);

    fullpath = g_build_path ("/", repo->worktree, event->new_path, NULL);
    // Check whether the renamed file is a symbolic link.
    if (seaf->ignore_symlinks && is_symlink(fullpath)) {
        g_free (fullpath);
        return;
    }
    g_free (fullpath);

    if (!is_path_writable(repo->id,
                          repo->is_readonly, event->path) ||
        !is_path_writable(repo->id,
                          repo->is_readonly, event->new_path)) {
        seaf_debug ("Rename: %s or %s is not writable, ignore.\n",
                    event->path, event->new_path);
        return;
    }

    if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                              repo->id, event->path)) {
        seaf_debug ("Rename: %s is locked on server, ignore.\n", event->path);
        /* send_sync_error_notification (repo->id, NULL, event->path, */
        /*                               SYNC_ERROR_ID_FILE_LOCKED); */
        return;
    }

    if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                              repo->id, event->new_path)) {
        seaf_debug ("Rename: %s is locked on server, ignore.\n", event->new_path);
        /* send_sync_error_notification (repo->id, NULL, event->new_path, */
        /*                               SYNC_ERROR_ID_FILE_LOCKED); */
        return;
    }

    src_ignored = check_full_path_ignore(repo->worktree, event->path, ignore_list);
    dst_ignored = check_full_path_ignore(repo->worktree, event->new_path, ignore_list);

    /* If the destination path is ignored, just remove the source path. */
    if (dst_ignored) {
        if (!src_ignored &&
            !is_seafile_backup_file (event->new_path) &&
            check_locked_file_before_remove (fset, event->path)) {
            not_found = FALSE;
            remove_from_index_with_prefix (istate, event->path, &not_found);
            if (not_found)
                scan_subtree_for_deletion (repo->id,
                                           istate,
                                           repo->worktree, event->path,
                                           ignore_list, fset,
                                           repo->is_readonly,
                                           scanned_del_dirs,
                                           repo->changeset);

            remove_from_changeset (repo->changeset,
                                   DIFF_STATUS_DELETED,
                                   event->path,
                                   FALSE,
                                   NULL);
        }
        return;
    }

    /* Now the destination path is not ignored. */

    if (!src_ignored && !ignore_xlsx_update (event->path, event->new_path) &&
        check_locked_file_before_remove (fset, event->path)) {
        not_found = FALSE;
        rename_index_entries (istate, event->path, event->new_path, &not_found,
                              NULL, NULL);
        if (not_found)
            scan_subtree_for_deletion (repo->id,
                                       istate,
                                       repo->worktree, event->path,
                                       ignore_list, fset,
                                       repo->is_readonly,
                                       scanned_del_dirs,
                                       repo->changeset);

        /* Moving files out of a dir may make it empty. */
        try_add_empty_parent_dir_entry_from_wt (repo->worktree,
                                                istate,
                                                ignore_list,
                                                event->path);

        add_to_changeset (repo->changeset,
                          DIFF_STATUS_RENAMED,
                          NULL,
                          NULL,
                          NULL,
                          event->path,
                          event->new_path);
    }

    AddOptions options;
    memset (&options, 0, sizeof(options));
    options.fset = fset;
    options.is_repo_ro = repo->is_readonly;
    options.changeset = repo->changeset;

    /* We should always scan the destination to compare with the renamed
     * index entries. For example, in the following case:
     * 1. file a.txt is updated;
     * 2. a.txt is moved to test/a.txt;
     * If the two operations are executed in a batch, the updated content
     * of a.txt won't be committed if we don't scan the destination, because
     * when we process the update event, a.txt is already not in its original
     * place.
     */
    add_recursive (repo->id, repo->version, repo->email,
                   istate, repo->worktree, event->new_path,
                   crypt, FALSE, ignore_list,
                   total_size, NULL, &options);
}

#ifdef WIN32

typedef struct FindOfficeData {
    const char *lock_file_name;
    char *office_file_name;
} FindOfficeData;

static int
find_office_file_cb (wchar_t *parent,
                     WIN32_FIND_DATAW *fdata,
                     void *user_data,
                     gboolean *stop)
{
    FindOfficeData *data = user_data;
    const wchar_t *dname_w = fdata->cFileName;
    wchar_t *lock_name_w = NULL;

    if (wcslen(dname_w) < 2)
        return 0;
    if (wcsncmp (dname_w, L"~$", 2) == 0)
        return 0;

    lock_name_w = g_utf8_to_utf16 (data->lock_file_name,
                                   -1, NULL, NULL, NULL);
    /* Skip "~$" at the beginning. */
    if (wcscmp (dname_w + 2, lock_name_w) == 0) {
        data->office_file_name = g_utf16_to_utf8 (dname_w, -1, NULL, NULL, NULL);
        *stop = TRUE;
    }
    g_free (lock_name_w);

    return 0;
}

static gboolean
find_office_file_path (const char *worktree,
                       const char *parent_dir,
                       const char *lock_file_name,
                       char **office_path)
{
    char *fullpath = NULL;
    wchar_t *fullpath_w = NULL;
    FindOfficeData data;
    gboolean ret = FALSE;

    fullpath = g_build_path ("/", worktree, parent_dir, NULL);
    fullpath_w = win32_long_path (fullpath);

    data.lock_file_name = lock_file_name;
    data.office_file_name = NULL;

    if (traverse_directory_win32 (fullpath_w, find_office_file_cb, &data) < 0) {
        goto out;
    }

    if (data.office_file_name != NULL) {
        *office_path = g_build_path ("/", parent_dir, data.office_file_name, NULL);
        ret = TRUE;
    }

out:
    g_free (fullpath);
    g_free (fullpath_w);
    return ret;
}

#elif defined __APPLE__

static gboolean
find_office_file_path (const char *worktree,
                       const char *parent_dir,
                       const char *lock_file_name,
                       char **office_path)
{
    GDir *dir = NULL;
    GError *error = NULL;
    char *fullpath = NULL;
    const char *dname;
    char *dname_nfc = NULL;
    char *dname_skip_head = NULL;
    gboolean ret = FALSE;

    fullpath = g_build_path ("/", worktree, parent_dir, NULL);
    dir = g_dir_open (fullpath, 0, &error);
    if (error) {
        seaf_warning ("Failed to open dir %s: %s.\n", fullpath, error->message);
        g_clear_error (&error);
        g_free (fullpath);
        return ret;
    }

    while ((dname = g_dir_read_name (dir)) != NULL) {
        dname_nfc = g_utf8_normalize (dname, -1, G_NORMALIZE_NFC);
        if (!dname_nfc)
            continue;

        if (g_utf8_strlen(dname_nfc, -1) < 2 || strncmp (dname_nfc, "~$", 2) == 0) {
            g_free (dname_nfc);
            continue;
        }

        dname_skip_head = g_utf8_find_next_char(g_utf8_find_next_char(dname_nfc, NULL), NULL);

        if (g_strcmp0 (dname_skip_head, lock_file_name) == 0) {
            *office_path = g_build_path ("/", parent_dir, dname_nfc, NULL);
            ret = TRUE;
            g_free (dname_nfc);
            break;
        }

        g_free (dname_nfc);
    }

    g_free (fullpath);
    g_dir_close (dir);
    return ret;
}

#else

static gboolean
find_office_file_path (const char *worktree,
                       const char *parent_dir,
                       const char *lock_file_name,
                       gboolean is_wps,
                       char **office_path)
{
    GDir *dir = NULL;
    GError *error = NULL;
    char *fullpath = NULL;
    const char *dname;
    char *dname_nfc = NULL;
    char *dname_skip_head = NULL;
    gboolean ret = FALSE;

    fullpath = g_build_path ("/", worktree, parent_dir, NULL);
    dir = g_dir_open (fullpath, 0, &error);
    if (error) {
        seaf_warning ("Failed to open dir %s: %s.\n", fullpath, error->message);
        g_clear_error (&error);
        g_free (fullpath);
        return ret;
    }

    while ((dname = g_dir_read_name (dir)) != NULL) {
        dname_nfc = g_utf8_normalize (dname, -1, G_NORMALIZE_NFC);
        if (!dname_nfc)
            continue;

        if (g_utf8_strlen(dname_nfc, -1) < 2 || strncmp (dname_nfc, "~$", 2) == 0 || strncmp(dname_nfc, ".~", 2) == 0) {
            g_free (dname_nfc);
            continue;
        }

        if (is_wps) {
            dname_skip_head = g_utf8_find_next_char(dname_nfc, NULL);
            // WPS may replace the first one or two characters of the filename with ".～" based on the length of the filename.
            if (strcmp (dname_skip_head, lock_file_name) == 0) {
                *office_path = g_build_path("/", parent_dir, dname_nfc, NULL);
                ret = TRUE;
                g_free (dname_nfc);
                break;
            }
            dname_skip_head = g_utf8_find_next_char(dname_skip_head, NULL);
        } else {
            dname_skip_head = g_utf8_find_next_char(g_utf8_find_next_char(dname_nfc, NULL), NULL);
        }

        if (g_strcmp0 (dname_skip_head, lock_file_name) == 0) {
            *office_path = g_build_path ("/", parent_dir, dname_nfc, NULL);
            ret = TRUE;
            g_free (dname_nfc);
            break;
        }

        g_free (dname_nfc);
    }

    g_free (fullpath);
    g_dir_close (dir);
    return ret;
}

#endif

#if defined WIN32 || defined __APPLE__

static gboolean
is_office_lock_file (const char *worktree,
                     const char *path,
                     char **office_path)
{
    gboolean ret;

    if (!g_regex_match (office_lock_pattern, path, 0, NULL))
        return FALSE;

    /* Replace ~$abc.docx with abc.docx */
    *office_path = g_regex_replace (office_lock_pattern,
                                    path, -1, 0,
                                    "\\1", 0, NULL);

    /* When the filename is long, sometimes the first two characters
       in the filename will be directly replaced with ~$.
       So if the office_path file doesn't exist, we have to match
       against all filenames in this directory, to find the office
       file's name.
    */
    char *fullpath = g_build_path ("/", worktree, *office_path, NULL);
    if (seaf_util_exists (fullpath)) {
        g_free (fullpath);
        return TRUE;
    }
    g_free (fullpath);

    char *lock_file_name = g_path_get_basename(*office_path);
    char *parent_dir = g_path_get_dirname(*office_path);
    if (strcmp(parent_dir, ".") == 0) {
        g_free (parent_dir);
        parent_dir = g_strdup("");
    }
    g_free (*office_path);
    *office_path = NULL;

    ret = find_office_file_path (worktree, parent_dir, lock_file_name,
                                 office_path);

    g_free (lock_file_name);
    g_free (parent_dir);
    return ret;
}

#else

static gboolean
is_office_lock_file (const char *worktree,
                     const char *path,
                     char **office_path)
{
    gboolean ret;
    gboolean is_wps = FALSE;

    if (!office_lock_pattern || !libre_office_lock_pattern || !wps_lock_pattern)
        return FALSE;

    if (g_regex_match (office_lock_pattern, path, 0, NULL)) {
        /* Replace ~$abc.docx with abc.docx */
        *office_path = g_regex_replace (office_lock_pattern,
                                        path, -1, 0,
                                        "\\1", 0, NULL);
    } else if (g_regex_match (libre_office_lock_pattern, path, 0, NULL)) {
        /* Replace .~lock.abc.docx# with abc.docx */
        *office_path = g_regex_replace (libre_office_lock_pattern,
                                        path, -1, 0,
                                        "\\1", 0, NULL);
    } else if (g_regex_match (wps_lock_pattern, path, 0, NULL)) {
        /* Replace .~abc.docx with abc.docx */
        *office_path = g_regex_replace (wps_lock_pattern,
                                        path, -1, 0,
                                        "\\1", 0, NULL);
        is_wps = TRUE;
    } else
        return FALSE;

    /* When the filename is long, sometimes the first two characters
       in the filename will be directly replaced with ~$.
       So if the office_path file doesn't exist, we have to match
       against all filenames in this directory, to find the office
       file's name.
    */
    char *fullpath = g_build_path ("/", worktree, *office_path, NULL);
    if (seaf_util_exists (fullpath)) {
        g_free (fullpath);
        return TRUE;
    }
    g_free (fullpath);

    char *lock_file_name = g_path_get_basename(*office_path);
    char *parent_dir = g_path_get_dirname(*office_path);
    if (strcmp(parent_dir, ".") == 0) {
        g_free (parent_dir);
        parent_dir = g_strdup("");
    }
    g_free (*office_path);
    *office_path = NULL;

    ret = find_office_file_path (worktree, parent_dir, lock_file_name,
                                 is_wps, office_path);

    g_free (lock_file_name);
    g_free (parent_dir);
    return ret;
}

#endif

typedef struct LockOfficeJob {
    char repo_id[37];
    char *path;
    gboolean lock;              /* False if unlock */
} LockOfficeJob;

static void
lock_office_job_free (LockOfficeJob *job)
{
    if (!job)
        return;
    g_free (job->path);
    g_free (job);
}

static void
do_lock_office_file (LockOfficeJob *job)
{
    SeafRepo *repo;
    char *fullpath = NULL;
    SeafStat st;

    repo = seaf_repo_manager_get_repo (seaf->repo_mgr, job->repo_id);
    if (!repo)
        return;

    fullpath = g_build_path ("/", repo->worktree, job->path, NULL);
    if (seaf_stat (fullpath, &st) < 0 || !S_ISREG(st.st_mode)) {
        g_free (fullpath);
        return;
    }
    g_free (fullpath);

    seaf_message ("Auto lock file %s/%s\n", repo->name, job->path);

    int status = seaf_filelock_manager_get_lock_status (seaf->filelock_mgr,
                                                        repo->id, job->path);
    if (status != FILE_NOT_LOCKED) {
        return;
    }

    if (http_tx_manager_lock_file (seaf->http_tx_mgr,
                                   repo->effective_host,
                                   repo->use_fileserver_port,
                                   repo->token,
                                   repo->id,
                                   job->path) < 0) {
        seaf_warning ("Failed to lock %s in repo %.8s on server.\n",
                      job->path, repo->id);
        return;
    }

    /* Mark file as locked locally so that the user can see the effect immediately. */
    seaf_filelock_manager_mark_file_locked (seaf->filelock_mgr, repo->id, job->path, LOCKED_AUTO);
}

static void
do_unlock_office_file (LockOfficeJob *job)
{
    SeafRepo *repo;
    char *fullpath = NULL;
    SeafStat st;

    repo = seaf_repo_manager_get_repo (seaf->repo_mgr, job->repo_id);
    if (!repo)
        return;

    fullpath = g_build_path ("/", repo->worktree, job->path, NULL);
    if (seaf_stat (fullpath, &st) < 0 || !S_ISREG(st.st_mode)) {
        g_free (fullpath);
        return;
    }
    g_free (fullpath);

    seaf_message ("Auto unlock file %s/%s\n", repo->name, job->path);

    int status = seaf_filelock_manager_get_lock_status (seaf->filelock_mgr,
                                                        repo->id, job->path);
    if (status != FILE_LOCKED_BY_ME_AUTO) {
        return;
    }

    if (http_tx_manager_unlock_file (seaf->http_tx_mgr,
                                     repo->effective_host,
                                     repo->use_fileserver_port,
                                     repo->token,
                                     repo->id,
                                     job->path) < 0) {
        seaf_warning ("Failed to unlock %s in repo %.8s on server.\n",
                      job->path, repo->id);
        return;
    }

    /* Mark file as unlocked locally so that the user can see the effect immediately. */
    seaf_filelock_manager_mark_file_unlocked (seaf->filelock_mgr, repo->id, job->path);
}

#if 0
static void
unlock_closed_office_files ()
{
    GList *locked_files, *ptr;
    SeafRepo *repo;
    FileLockInfo *info;
    LockOfficeJob *job;

    locked_files = seaf_filelock_manager_get_auto_locked_files (seaf->filelock_mgr);
    for (ptr = locked_files; ptr; ptr = ptr->next) {
        info = ptr->data;

        seaf_message ("%s %s.\n", info->repo_id, info->path);

        repo = seaf_repo_manager_get_repo (seaf->repo_mgr, info->repo_id);
        if (!repo)
            continue;

        seaf_message ("1\n");

        if (!do_check_file_locked (info->path, repo->worktree, FALSE)) {
            seaf_message ("2\n");

            job = g_new0 (LockOfficeJob, 1);
            memcpy (job->repo_id, info->repo_id, 36);
            job->path = g_strdup(info->path);
            do_unlock_office_file (job);
            lock_office_job_free (job);
        }
    }

    g_list_free_full (locked_files, (GDestroyNotify)file_lock_info_free);
}
#endif

static void *
lock_office_file_worker (void *vdata)
{
    GAsyncQueue *queue = (GAsyncQueue *)vdata;
    LockOfficeJob *job;

    /* unlock_closed_office_files (); */

    while (1) {
        job = g_async_queue_pop (queue);
        if (!job)
            break;

        if (job->lock)
            do_lock_office_file (job);
        else
            do_unlock_office_file (job);

        lock_office_job_free (job);
    }

    return NULL;
}

static void
lock_office_file_on_server (SeafRepo *repo, const char *path)
{
    LockOfficeJob *job;
    GAsyncQueue *queue = seaf->repo_mgr->priv->lock_office_job_queue;

    if (!seaf_repo_manager_server_is_pro (seaf->repo_mgr, repo->server_url))
        return;

    job = g_new0 (LockOfficeJob, 1);
    memcpy (job->repo_id, repo->id, 36);
    job->path = g_strdup(path);
    job->lock = TRUE;

    g_async_queue_push (queue, job);
}

static void
unlock_office_file_on_server (SeafRepo *repo, const char *path)
{
    LockOfficeJob *job;
    GAsyncQueue *queue = seaf->repo_mgr->priv->lock_office_job_queue;

    if (!seaf_repo_manager_server_is_pro (seaf->repo_mgr, repo->server_url))
        return;

    job = g_new0 (LockOfficeJob, 1);
    memcpy (job->repo_id, repo->id, 36);
    job->path = g_strdup(path);
    job->lock = FALSE;

    g_async_queue_push (queue, job);
}

static int
apply_worktree_changes_to_index (SeafRepo *repo, struct index_state *istate,
                                 SeafileCrypt *crypt, GList *ignore_list,
                                 LockedFileSet *fset, GList **event_list)
{
    WTStatus *status;
    WTEvent *event, *next_event;
    gboolean not_found;
    char *office_path = NULL;

    status = seaf_wt_monitor_get_worktree_status (seaf->wt_monitor, repo->id);
    if (!status) {
        seaf_warning ("Can't find worktree status for repo %s(%.8s).\n",
                      repo->name, repo->id);
        return -1;
    }

    update_path_sync_status (repo, status, istate, ignore_list);

    GList *scanned_dirs = NULL, *scanned_del_dirs = NULL;

    WTEvent *last_event;

    pthread_mutex_lock (&status->q_lock);
    last_event = g_queue_peek_tail (status->event_q);
    pthread_mutex_unlock (&status->q_lock);

    if (!last_event) {
        seaf_message ("All events are processed for repo %s.\n", repo->id);
        status->partial_commit = FALSE;
        goto out;
    }

    gint64 total_size = 0;

    while (1) {
        pthread_mutex_lock (&status->q_lock);
        event = g_queue_pop_head (status->event_q);
        next_event = g_queue_peek_head (status->event_q);
        pthread_mutex_unlock (&status->q_lock);
        if (!event)
            break;

        WTEvent *copy = wt_event_new (event->ev_type, event->path, event->new_path);
        *event_list = g_list_prepend (*event_list, copy);

#ifdef WIN32
        // If a file or dir is moved, REMOVED and ADDED event will be emitted by the kernel.
        // When the kernel first emits the REMOVED event of the file or dir, and then emits the ADDED event of the file or dir,
        // it indicates that this is a move event.
        if (next_event && event->ev_type == WT_EVENT_DELETE) {
            char *event_base_name = g_path_get_basename (event->path);
            char *next_event_base_name  = g_path_get_basename (next_event->path);
            if (next_event->ev_type == WT_EVENT_CREATE_OR_UPDATE && g_strcmp0(event_base_name, next_event_base_name) == 0) {
                WTEvent *new_event =  wt_event_new (WT_EVENT_RENAME, event->path, next_event->path);

                pthread_mutex_lock (&status->q_lock);
                next_event = g_queue_pop_head (status->event_q);
                pthread_mutex_unlock (&status->q_lock);

                wt_event_free (event);
                wt_event_free (next_event);

                event = new_event;
            }
            g_free (event_base_name);
            g_free (next_event_base_name);
        }
#endif

        /* Scanned dirs list is used to avoid redundant scan of consecutive
           CREATE_OR_UPDATE events. When we see other events, we should
           clear the list. Otherwise in some cases we'll get wrong result.
           For example, the following sequence (run with a script):
           1. Add a dir with files
           2. Delete the dir with files
           3. Add back the same dir again.
        */
        if (event->ev_type != WT_EVENT_CREATE_OR_UPDATE) {
            g_list_free_full (scanned_dirs, g_free);
            scanned_dirs = NULL;
        }

        switch (event->ev_type) {
        case WT_EVENT_CREATE_OR_UPDATE:
            /* If consecutive CREATE_OR_UPDATE events present
               in the event queue, only process the last one.
            */
            if (next_event &&
                next_event->ev_type == event->ev_type &&
                strcmp (next_event->path, event->path) == 0)
                break;

            /* CREATE_OR_UPDATE event tells us the exact path of changed file/dir.
             * If the event path is not writable, we don't need to check the paths
             * under the event path.
             */
            if (!is_path_writable(repo->id,
                                  repo->is_readonly, event->path)) {
                char *filename = g_path_get_basename (event->path);
                if (seaf_repo_manager_is_ignored_hidden_file(filename)) {
                    g_free (filename);
                    break;
                }
                g_free (filename);

                char *fullpath = g_build_path(PATH_SEPERATOR, repo->worktree, event->path, NULL);                
                struct cache_entry *ce = index_name_exists(istate, event->path, strlen(event->path), 0);
                SeafStat st;
                if (ce != NULL &&
                    seaf_stat (fullpath, &st) == 0 &&
                    ce->ce_mtime.sec == st.st_mtime &&
                    ce->ce_size == st.st_size) {
                    g_free (fullpath);
                    break;
                }

                send_file_sync_error_notification (repo->id, repo->name, event->path,
                                                   SYNC_ERROR_ID_UPDATE_TO_READ_ONLY_REPO);
                seaf_debug ("%s is not writable, ignore.\n", event->path);

                g_free (fullpath);
                break;
            }

            office_path = NULL;
            if (is_office_lock_file (repo->worktree, event->path, &office_path))
                lock_office_file_on_server (repo, office_path);
            g_free (office_path);

            if (handle_add_files (repo, istate, crypt, ignore_list,
                                  fset,
                                  status, event,
                                  &scanned_dirs, &total_size))
                goto out;

            break;
        case WT_EVENT_SCAN_DIR:
            if (handle_add_files (repo, istate, crypt, ignore_list,
                                  fset,
                                  status, event,
                                  &scanned_dirs, &total_size))
                goto out;

            break;
        case WT_EVENT_DELETE:
            seaf_sync_manager_delete_active_path (seaf->sync_mgr,
                                                  repo->id,
                                                  event->path);

            office_path = NULL;
            if (is_office_lock_file (repo->worktree, event->path, &office_path))
                unlock_office_file_on_server (repo, office_path);
            g_free (office_path);

            if (check_full_path_ignore(repo->worktree, event->path, ignore_list))
                break;

            if (!is_path_writable(repo->id,
                                  repo->is_readonly, event->path)) {
                seaf_debug ("%s is not writable, ignore.\n", event->path);
                break;
            }

            if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                                      repo->id, event->path)) {
                seaf_debug ("Delete: %s is locked on server, ignore.\n", event->path);
                /* send_sync_error_notification (repo->id, NULL, event->path, */
                /*                               SYNC_ERROR_ID_FILE_LOCKED); */
                break;
            }

            if (check_locked_file_before_remove (fset, event->path)) {
                not_found = FALSE;
                remove_from_index_with_prefix (istate, event->path, &not_found);
                if (not_found)
                    scan_subtree_for_deletion (repo->id,
                                               istate,
                                               repo->worktree, event->path,
                                               ignore_list, fset,
                                               repo->is_readonly,
                                               &scanned_del_dirs,
                                               repo->changeset);

                remove_from_changeset (repo->changeset,
                                       DIFF_STATUS_DELETED,
                                       event->path,
                                       FALSE,
                                       NULL);

                try_add_empty_parent_dir_entry_from_wt (repo->worktree,
                                                        istate,
                                                        ignore_list,
                                                        event->path);
            }
            break;
        case WT_EVENT_RENAME:
            handle_rename (repo, istate, crypt, ignore_list, fset, event, &scanned_del_dirs, &total_size);
            break;
        case WT_EVENT_ATTRIB:
            if (!is_path_writable(repo->id,
                                  repo->is_readonly, event->path)) {
                seaf_debug ("%s is not writable, ignore.\n", event->path);
                break;
            }
            update_attributes (repo, istate, repo->worktree, event->path);
            break;
        case WT_EVENT_OVERFLOW:
            seaf_warning ("Kernel event queue overflowed, fall back to scan.\n");
            scan_worktree_for_changes (istate, repo, crypt, ignore_list, fset);
            break;
        }

        if (event == last_event) {
            wt_event_free (event);
            seaf_message ("All events are processed for repo %s.\n", repo->id);
            status->partial_commit = FALSE;
            break;
        } else
            wt_event_free (event);
    }

out:
    wt_status_unref (status);
    string_list_free (scanned_dirs);
    string_list_free (scanned_del_dirs);

    return 0;
}

static int
index_add (SeafRepo *repo, struct index_state *istate,
           gboolean is_force_commit, GList **event_list)
{
    SeafileCrypt *crypt = NULL;
    LockedFileSet *fset = NULL;
    GList *ignore_list = NULL;
    int ret = 0;

    if (repo->encrypted) {
        crypt = seafile_crypt_new (repo->enc_version, repo->enc_key, repo->enc_iv);
    }

#if defined WIN32 || defined __APPLE__
    if (repo->version > 0)
        fset = seaf_repo_manager_get_locked_file_set (seaf->repo_mgr, repo->id);
#endif

    ignore_list = seaf_repo_load_ignore_files (repo->worktree);

    if (!is_force_commit) {
        if (apply_worktree_changes_to_index (repo, istate, crypt, ignore_list, fset, event_list) < 0) {
            seaf_warning ("Failed to apply worktree changes to index.\n");
            ret = -1;
        }
    } else if (scan_worktree_for_changes (istate, repo, crypt, ignore_list, fset) < 0) {
        seaf_warning ("Failed to scan worktree for changes.\n");
        ret = -1;
    }

    seaf_repo_free_ignore_files (ignore_list);

#if defined WIN32 || defined __APPLE__
    locked_file_set_free (fset);
#endif

    g_free (crypt);

    return ret;
}

static int
commit_tree (SeafRepo *repo, const char *root_id,
             const char *desc, char commit_id[])
{
    SeafCommit *commit;

    commit = seaf_commit_new (NULL, repo->id, root_id,
                              repo->email ? repo->email
                              : "unknown",
                              seaf->client_id,
                              desc, 0);

    commit->parent_id = g_strdup (repo->head->commit_id);

    /* Add this computer's name to commit. */
    commit->device_name = g_strdup(seaf->client_name);
    commit->client_version = g_strdup (SEAFILE_CLIENT_VERSION);

    seaf_repo_to_commit (repo, commit);

    if (seaf_commit_manager_add_commit (seaf->commit_mgr, commit) < 0)
        return -1;

    seaf_branch_set_commit (repo->head, commit->commit_id);
    seaf_branch_manager_update_branch (seaf->branch_mgr, repo->head);

    strcpy (commit_id, commit->commit_id);
    seaf_commit_unref (commit);

    return 0;
}

static gboolean
compare_index_changeset (struct index_state *istate, ChangeSet *changeset)
{
    struct cache_entry *ce;
    int i;
    gboolean ret = TRUE;

    for (i = 0; i < istate->cache_nr; ++i) {
        ce = istate->cache[i];

        if (!(ce->ce_flags & CE_ADDED))
            continue;

        seaf_message ("checking %s in changeset.\n", ce->name);

        if (!changeset_check_path (changeset, ce->name,
                                   ce->sha1, ce->ce_mode, ce->ce_mtime.sec))
            ret = FALSE;
    }

    return ret;
}

#if 0
static int 
print_index (struct index_state *istate)
{
    int i;
    struct cache_entry *ce;
    char id[41];
    seaf_message ("Totally %u entries in index, version %u.\n",
                  istate->cache_nr, istate->version);
    for (i = 0; i < istate->cache_nr; ++i) {
        ce = istate->cache[i];
        rawdata_to_hex (ce->sha1, id, 20);
        seaf_message ("%s, %s, %o, %"G_GINT64_FORMAT", %s, %"G_GINT64_FORMAT", %d\n",
                      ce->name, id, ce->ce_mode, 
                      ce->ce_mtime.sec, ce->modifier, ce->ce_size, ce_stage(ce));
    }

    return 0;
}
#endif

static void
print_event_log (const char *repo_name, const char *repo_id,
                 const char *commit_id, GList *event_list)
{
    GList *ptr;
    WTEvent *event;
    char *name;
    int i = 0;
    GString *msg = g_string_new ("");

    g_string_append_printf (msg, "%s %s %s\n", repo_name, repo_id, commit_id);

    for (ptr = event_list; ptr; ptr = ptr->next) {
        event = ptr->data;
        i++;
        switch (event->ev_type) {
        case WT_EVENT_CREATE_OR_UPDATE:
            name = "create/update";
            break;
        case WT_EVENT_SCAN_DIR:
            name = "scan dir";
            break;
        case WT_EVENT_DELETE:
            name = "delete";
            break;
        case WT_EVENT_RENAME:
            name = "rename";
            break;
        case WT_EVENT_OVERFLOW:
            name = "overflow";
            break;
        default:
            name = "unknown";
        }
        g_string_append_printf (msg, "[event %d] %s, %s %s\n", i, name, event->path, event->new_path?event->new_path:"");
    }

    seafile_event_message (msg->str);

    g_string_free (msg, TRUE);
}

char *
seaf_repo_index_commit (SeafRepo *repo,
                        gboolean is_force_commit,
                        gboolean is_initial_commit,
                        GError **error)
{
    SeafRepoManager *mgr = repo->manager;
    struct index_state istate;
    char index_path[SEAF_PATH_MAX];
    SeafCommit *head = NULL;
    char *new_root_id = NULL;
    char commit_id[41];
    ChangeSet *changeset = NULL;
    GList *diff_results = NULL;
    char *desc = NULL;
    char *ret = NULL;
    GList *event_list = NULL;

    if (!check_worktree_common (repo))
        return NULL;

    memset (&istate, 0, sizeof(istate));
    snprintf (index_path, SEAF_PATH_MAX, "%s/%s", mgr->index_dir, repo->id);
    if (read_index_from (&istate, index_path, repo->version) < 0) {
        seaf_warning ("Failed to load index.\n");
        g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal data structure error");
        return NULL;
    }

    changeset = changeset_new (repo->id);
    if (!changeset) {
        g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal data structure error");
        goto out;
    }

    repo->changeset = changeset;

    if (index_add (repo, &istate, is_force_commit, &event_list) < 0) {
        g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL, "Failed to add");
        goto out;
    }

    if (!istate.cache_changed)
        goto out;

    new_root_id = commit_tree_from_changeset (changeset);
    if (!new_root_id) {
        seaf_warning ("Create commit tree failed for repo %s\n", repo->id);
        g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_GENERAL,
                     "Failed to generate commit");
        goto out;
    }

    head = seaf_commit_manager_get_commit (seaf->commit_mgr,
                                           repo->id, repo->version,
                                           repo->head->commit_id);
    if (!head) {
        seaf_warning ("Head commit %s for repo %s not found\n",
                      repo->head->commit_id, repo->id);
        g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Data corrupt");
        goto out;
    }

    if (strcmp (head->root_id, new_root_id) == 0) {
        seaf_message ("No change to the fs tree of repo %s\n", repo->id);
        /* If no file modification and addition are missing, and the new root
         * id is the same as the old one, skip commiting.
         */
        if (!is_initial_commit && !is_force_commit)
            compare_index_changeset (&istate, changeset);

        update_index (&istate, index_path);
        goto out;
    }

    diff_commit_roots (repo->id, repo->version, head->root_id, new_root_id, &diff_results, TRUE);
    desc = diff_results_to_description (diff_results);
    if (!desc)
        desc = g_strdup("");

    if (commit_tree (repo, new_root_id, desc, commit_id) < 0) {
        seaf_warning ("Failed to save commit file");
        g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal error");
        goto out;
    }

    if (event_list) {
        event_list = g_list_reverse (event_list);
        print_event_log (repo->name, repo->id, commit_id, event_list);
    }

    if (update_index (&istate, index_path) < 0) {
        g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_INTERNAL, "Internal error");
        goto out;
    }

    g_signal_emit_by_name (seaf, "repo-committed", repo);

    ret = g_strdup(commit_id);

out:
    if (event_list) {
        g_list_free_full (event_list, (GDestroyNotify)wt_event_free);
    }
    g_free (desc);
    seaf_commit_unref (head);
    g_free (new_root_id);
    changeset_free (changeset);
    g_list_free_full (diff_results, (GDestroyNotify)diff_entry_free);
    discard_index (&istate);
    return ret;
}

#ifdef DEBUG_UNPACK_TREES
static void
print_unpack_result (struct index_state *result)
{
	int i;
	struct cache_entry *ce;

	for (i = 0; i < result->cache_nr; ++i) {
		ce = result->cache[i];
		printf ("%s\t", ce->name);
		if (ce->ce_flags & CE_UPDATE)
			printf ("update/add\n");
		else if (ce->ce_flags & CE_WT_REMOVE)
			printf ("remove\n");
		else
			printf ("unchange\n");
	}
}

static int 
print_index (struct index_state *istate)
{
    printf ("Index timestamp: %d\n", istate->timestamp.sec);

    int i;
    struct cache_entry *ce;
    char id[41];
    printf ("Totally %u entries in index.\n", istate->cache_nr);
    for (i = 0; i < istate->cache_nr; ++i) {
        ce = istate->cache[i];
        rawdata_to_hex (ce->sha1, id, 20);
        printf ("%s\t%s\t%o\t%d\t%d\n", ce->name, id, ce->ce_mode, 
                ce->ce_ctime.sec, ce->ce_mtime.sec);
    }

    return 0;
}
#endif  /* DEBUG_UNPACK_TREES */

GList *
seaf_repo_diff (SeafRepo *repo, const char *old, const char *new, int fold_dir_diff, char **error)
{
    SeafCommit *c1 = NULL, *c2 = NULL;
    int ret = 0;
    GList *diff_entries = NULL;

    c2 = seaf_commit_manager_get_commit (seaf->commit_mgr,
                                         repo->id, repo->version,
                                         new);
    if (!c2) {
        *error = g_strdup("Can't find new commit");
        return NULL;
    }

    if (old == NULL || old[0] == '\0') {
        if (c2->parent_id && c2->second_parent_id) {
            ret = diff_merge (c2, &diff_entries, fold_dir_diff);
            seaf_commit_unref (c2);
            if (ret < 0) {
                *error = g_strdup("Failed to do diff");
                g_list_free_full (diff_entries, (GDestroyNotify)diff_entry_free);
                return NULL;
            }
            return diff_entries;
        }

        if (!c2->parent_id) {
            seaf_commit_unref (c2);
            return NULL;
        }
        c1 = seaf_commit_manager_get_commit (seaf->commit_mgr,
                                             repo->id, repo->version,
                                             c2->parent_id);
    } else {
        c1 = seaf_commit_manager_get_commit (seaf->commit_mgr,
                                             repo->id, repo->version, old);
    }

    if (!c1) {
        *error = g_strdup("Can't find old commit");
        seaf_commit_unref (c2);
        return NULL;
    }

    /* do diff */
    ret = diff_commits (c1, c2, &diff_entries, fold_dir_diff);
    if (ret < 0) {
        g_list_free_full (diff_entries, (GDestroyNotify)diff_entry_free);
        diff_entries = NULL;
        *error = g_strdup("Failed to do diff");
    }

    seaf_commit_unref (c1);
    seaf_commit_unref (c2);

    return diff_entries;
}

int
checkout_empty_dir (const char *worktree,
                    const char *name,
                    gint64 mtime,
                    struct cache_entry *ce)
{
    char *path;

    path = build_checkout_path (worktree, name, strlen(name));

    if (!path)
        return FETCH_CHECKOUT_FAILED;

    if (!seaf_util_exists (path) && seaf_util_mkdir (path, 0777) < 0) {
        seaf_warning ("Failed to create empty dir %s in checkout.\n", path);
        g_free (path);
        return FETCH_CHECKOUT_FAILED;
    }

    if (mtime != 0 && seaf_set_file_time (path, mtime) < 0) {
        seaf_warning ("Failed to set mtime for %s.\n", path);
    }

    SeafStat st;
    seaf_stat (path, &st);
    fill_stat_cache_info (ce, &st);

    g_free (path);
    return FETCH_CHECKOUT_SUCCESS;
}

static struct cache_entry *
cache_entry_from_diff_entry (DiffEntry *de)
{
    int size, namelen;
    struct cache_entry *ce;

    namelen = strlen(de->name);
    size = cache_entry_size(namelen);
    ce = calloc(1, size);
    memcpy(ce->name, de->name, namelen);
    ce->ce_flags = namelen;

    memcpy (ce->sha1, de->sha1, 20);
    ce->modifier = g_strdup(de->modifier);
    ce->ce_size = de->size;
    ce->ce_mtime.sec = de->mtime;

    if (S_ISREG(de->mode))
        ce->ce_mode = create_ce_mode (de->mode);
    else
        ce->ce_mode = S_IFDIR;

    return ce;
}

#define UPDATE_CACHE_SIZE_LIMIT 100 * (1 << 20) /* 100MB */

typedef struct FileTxData {
    char repo_id[37];
    int repo_version;
    SeafileCrypt *crypt;
    HttpTxTask *http_task;
    char conflict_head_id[41];
    GAsyncQueue *finished_tasks;

    const char *worktree;
    LockedFileSet *fset;
} FileTxData;

typedef struct FileTxTask {
    char *path;
    struct cache_entry *ce;
    DiffEntry *de;
    gboolean new_ce;
    gboolean skip_fetch;

    int result;
    gboolean no_checkout;
    gboolean force_conflict;
} FileTxTask;

static void
file_tx_task_free (FileTxTask *task)
{
    if (!task)
        return;

    g_free (task->path);
    g_free (task);
}

struct _UpdateAux {
    int fd;
    SeafileCrypt *crypt;
    // The older version client might fail to load the repo key, resulting in blocks being encrypted with an all-zero enc key.
    // When decryption fails, an additional attempt is made to decrypt using the all-zero key.
    SeafileCrypt *zero_crypt;
    char *repo_id;
    char *content;
    int size;
    void *user_data;
};
typedef struct _UpdateAux UpdateAux;

static int
fill_block (void *contents, size_t realsize, void *userp)
{
    UpdateAux *aux = userp;
    int rc = 0;
    int ret = realsize;
    char *dec_out = NULL;
    int dec_out_len = -1;

    if (aux->crypt) {
        rc = seafile_decrypt (&dec_out, &dec_out_len, contents, realsize, aux->crypt);
        if (rc != 0) {
            rc = seafile_decrypt(&dec_out, &dec_out_len, contents, realsize, aux->zero_crypt);
            if (rc != 0) {
                seaf_warning ("Decrypt block failed.\n");
                return -1;
            } else {
                save_repo_property (seaf->repo_mgr, aux->repo_id, REPO_PROP_EMPTY_ENC_KEY, "true");
            }
        }

        ret = writen (aux->fd, dec_out, dec_out_len);
    } else {
        ret = writen (aux->fd, contents, realsize);
    }

    g_free (dec_out);

    return ret;
}

static size_t
update_block_cb (void *contents, size_t size, size_t nmemb, void *userp)
{
    size_t realsize = size * nmemb;
    UpdateAux *aux = userp;
    HttpTxTask *task = aux->user_data;
    int ret = realsize;

    if (task) {
        if (task->state == HTTP_TASK_STATE_CANCELED || task->all_stop) {
            return 0;
        }
    }

    aux->size += realsize;
    if (fill_block (contents, realsize, aux) < 0) {
        return 0;
    }

    /* Update global transferred bytes. */
    g_atomic_int_add (&(seaf->sync_mgr->recv_bytes), realsize);

    /* Update transferred bytes for this task */
    if (task)
        g_atomic_int_add (&task->tx_bytes, realsize);

    /* If uploaded bytes exceeds the limit, wait until the counter
     * is reset. We check the counter every 100 milliseconds, so we
     * can waste up to 100 milliseconds without sending data after
     * the counter is reset.
     */
    while (1) {
        gint sent = g_atomic_int_get(&(seaf->sync_mgr->recv_bytes));
        if (seaf->sync_mgr->download_limit > 0 &&
            sent > seaf->sync_mgr->download_limit)
            /* 100 milliseconds */
            g_usleep (100000);
        else
            break;
    }

    return ret;
}

static size_t
update_enc_block_cb (void *contents, size_t size, size_t nmemb, void *userp)
{
    size_t realsize = size * nmemb;
    UpdateAux *aux = userp;
    HttpTxTask *task = aux->user_data;
    int ret = realsize;

    if (task) {
        if (task->state == HTTP_TASK_STATE_CANCELED || task->all_stop) {
            return 0;
        }
    }

    aux->content = g_realloc (aux->content, aux->size + realsize);
    if (!aux->content) {
        seaf_warning ("Not enough memory.\n");
        return 0;
    }
    memcpy (aux->content + aux->size, contents, realsize);
    aux->size += realsize;

    /* Update global transferred bytes. */
    g_atomic_int_add (&(seaf->sync_mgr->recv_bytes), realsize);

    /* Update transferred bytes for this task */
    if (task)
        g_atomic_int_add (&task->tx_bytes, realsize);

    /* If uploaded bytes exceeds the limit, wait until the counter
     * is reset. We check the counter every 100 milliseconds, so we
     * can waste up to 100 milliseconds without sending data after
     * the counter is reset.
     */
    while (1) {
        gint sent = g_atomic_int_get(&(seaf->sync_mgr->recv_bytes));
        if (seaf->sync_mgr->download_limit > 0 &&
            sent > seaf->sync_mgr->download_limit)
            /* 100 milliseconds */
            g_usleep (100000);
        else
            break;
    }

    return ret;
}

static int
checkout_block_cb (const char *repo_id, const char *block_id, int fd, SeafileCrypt *crypt, CheckoutBlockAux *user_data)
{
    HttpTxTask *task = user_data->task;
    int ret = 0;
    int error_id = SYNC_ERROR_ID_NO_ERROR;
    SeafileCrypt *zero_crypt = NULL;
    UpdateAux aux = {0};
    aux.fd = fd;
    aux.crypt = crypt;
    aux.repo_id = repo_id;
    aux.user_data = task;
    if (crypt) {
        unsigned char enc_key[32], enc_iv[16];
        memset (enc_key, 0, sizeof(enc_key)); 
        memset (enc_iv, 0, sizeof(enc_iv)); 
        zero_crypt = seafile_crypt_new (crypt->version, enc_key, enc_iv);
        aux.zero_crypt = zero_crypt;
        if (http_tx_manager_get_block(seaf->http_tx_mgr, user_data->repo_id,
                                      block_id, user_data->host,
                                      user_data->token, user_data->use_fileserver_port,
                                      &error_id,
                                      update_enc_block_cb, &aux) < 0) {
            if (task->state == HTTP_TASK_STATE_CANCELED) {
                ret = -1;
                goto out;
            }
            if (task->error == SYNC_ERROR_ID_NO_ERROR) {
                task->error = error_id;
            }
            ret = -1;
            seaf_warning ("Failed to get block %s from server.\n",
                          block_id);
            goto out;
        }
        if (fill_block(aux.content, aux.size, &aux) < 0) {
            ret = -1;
            seaf_warning ("Failed to fill block %s.\n",
                          block_id);
            goto out;
        }
    } else {
        if (http_tx_manager_get_block(seaf->http_tx_mgr, user_data->repo_id,
                                      block_id, user_data->host,
                                      user_data->token, user_data->use_fileserver_port,
                                      &error_id,
                                      update_block_cb, &aux) < 0) {
            if (task->state == HTTP_TASK_STATE_CANCELED) {
                ret = -1;
                goto out;
            }
            if (task->error == SYNC_ERROR_ID_NO_ERROR) {
                task->error = error_id;
            }
            ret = -1;
            seaf_warning ("Failed to get block %s from server.\n",
                          block_id);
            goto out;
        }
    }
    if (task)
        task->done_download += aux.size;
out:
    if (zero_crypt)
        g_free (zero_crypt);
    g_free (aux.content);
    return ret;
}

static gboolean
check_path_conflict (const char *path, char **orig_path)
{
    gboolean is_conflict = FALSE;
    GError *error = NULL;

    is_conflict = g_regex_match (conflict_pattern, path, 0, NULL);
    if (is_conflict) {
        *orig_path = g_regex_replace_literal (conflict_pattern, path, -1,
                                              0, "", 0, &error);
        if (!*orig_path)
            is_conflict = FALSE;
    }

    return is_conflict;
}

/*
static void
cleanup_file_blocks_http (HttpTxTask *task, const char *file_id)
{
    Seafile *file;
    int i;
    char *block_id;
    int *pcnt;

    file = seaf_fs_manager_get_seafile (seaf->fs_mgr,
                                        task->repo_id, task->repo_version,
                                        file_id);
    if (!file) {
        seaf_warning ("Failed to load seafile object %s:%s\n",
                      task->repo_id, file_id);
        return;
    }

    for (i = 0; i < file->n_blocks; ++i) {
        block_id = file->blk_sha1s[i];

        pthread_mutex_lock (&task->ref_cnt_lock);

        pcnt = g_hash_table_lookup (task->blk_ref_cnts, block_id);
        if (pcnt) {
            --(*pcnt);
            if (*pcnt > 0) {
                pthread_mutex_unlock (&task->ref_cnt_lock);
                continue;
            }
        }

        seaf_block_manager_remove_block (seaf->block_mgr,
                                         task->repo_id, task->repo_version,
                                         block_id);
        g_hash_table_remove (task->blk_ref_cnts, block_id);

        pthread_mutex_unlock (&task->ref_cnt_lock);
    }

    seafile_unref (file);
}
*/

// just skip all downloaded blocks.
static void
calculate_block_offset(Seafile *file, gint64 *block_map, gint64 *skip_buffer_size, int *block_offset, gint64 requested_offset)
{
    gint64 offset = 0;
    int i = 0;
    *skip_buffer_size = 0;
    for (; i < file->n_blocks ; ++i) {
        offset += block_map[i];
        if (offset > requested_offset)
            break;
        *skip_buffer_size = offset;
    }
    *block_offset = i;
}

#define CACHE_BLOCK_MAP_THRESHOLD (2 << 23) /* 8MB */

static void
check_and_get_block_offset (SeafRepoManager *mgr, CheckoutBlockAux *aux,
                            const char *repo_id, int version,
                            const char *file_id, const char *file_path,
                            gint64 *skip_buffer_size, int *block_offset)
{
    Seafile *seafile = NULL;
    SeafStat st;
    int n_blocks = 0;
    gint64 *block_map = NULL;
    char *tmp_path = NULL;
    gboolean path_exists = FALSE;

    seafile = seaf_fs_manager_get_seafile (seaf->fs_mgr, repo_id, version, file_id);
    if (!seafile) {
        return;
    }

    if (seafile->file_size <= CACHE_BLOCK_MAP_THRESHOLD) {
        goto out;
    }

    tmp_path = g_strconcat (file_path, SEAF_TMP_EXT, NULL);

    path_exists = (seaf_stat (tmp_path, &st) == 0);
    if (!path_exists) {
        goto out;
    }

    char file_id_attr[41];
    ssize_t len;

    len = seaf_getxattr (tmp_path, SEAFILE_FILE_ID_ATTR,
                         file_id_attr, sizeof(file_id_attr));
    if (len < 0) {
        goto out;
    }
    // File has been changed on server.
    if (g_strcmp0 (file_id, file_id_attr) != 0) {
        goto out;
    }

    pthread_rwlock_rdlock (&mgr->priv->block_map_lock);
    block_map = g_hash_table_lookup (mgr->priv->block_map_cache_table, file_id);
    pthread_rwlock_unlock (&mgr->priv->block_map_lock);
    if (!block_map) {
        if (http_tx_manager_get_file_block_map (seaf->http_tx_mgr,
                                                repo_id,
                                                file_id,
                                                aux->host,
                                                aux->token,
                                                aux->use_fileserver_port,
                                                &block_map,
                                                &n_blocks) == 0) {
            if (n_blocks != seafile->n_blocks) {
                seaf_warning ("Block number return from server does not match"
                              "seafile object. File-id is %s."
                              "Returned %d, expect %d\n",
                              seafile->file_id, n_blocks, seafile->n_blocks);
            } else {
                pthread_rwlock_wrlock (&mgr->priv->block_map_lock);
                g_hash_table_replace (mgr->priv->block_map_cache_table, g_strdup(file_id), block_map);
                pthread_rwlock_unlock (&mgr->priv->block_map_lock);
            }
        }

    }
    if (block_map) {
        calculate_block_offset(seafile, block_map, skip_buffer_size, block_offset, st.st_size);
    }
out:
    g_free (tmp_path);
    seafile_unref (seafile);
}

int
seaf_repo_manager_checkout_file (SeafRepo *repo,
                                 const char *file_id,
                                 const char *file_path,
                                 guint32 mode,
                                 guint64 mtime,
                                 SeafileCrypt *crypt,
                                 const char *in_repo_path,
                                 const char *conflict_head_id,
                                 gboolean force_conflict,
                                 gboolean *conflicted,
                                 const char *email,
                                 CheckoutBlockAux *aux)
{
    int ret;
    gint64 skip_buffer_size = 0;
    int block_offset = 0;

    if (!crypt)
        check_and_get_block_offset (seaf->repo_mgr, aux, repo->id, repo->version,
                                    file_id, file_path, &skip_buffer_size, &block_offset);

    FileCheckoutData file_data = {0}; 
    int error_id = FETCH_CHECKOUT_SUCCESS;
    file_data.repo_id = repo->id;
    file_data.version = repo->version;
    file_data.file_id = file_id; 
    file_data.file_path = file_path;
    file_data.mode = mode;
    file_data.mtime = mtime;
    file_data.crypt = crypt;
    file_data.in_repo_path = in_repo_path;
    file_data.conflict_head_id = conflict_head_id;
    file_data.force_conflict = force_conflict;
    file_data.conflicted = conflicted;
    file_data.email = email;
    file_data.skip_buffer_size = skip_buffer_size;
    file_data.block_offset = block_offset;

    ret = seaf_fs_manager_checkout_file (seaf->fs_mgr,
                                         &file_data,
                                         &error_id,
                                         checkout_block_cb,
                                         aux);
    return ret;
}

static int
checkout_file_http (FileTxData *data,
                    FileTxTask *file_task)
{
    char *repo_id = data->repo_id;
    int repo_version = data->repo_version;
    struct cache_entry *ce = file_task->ce;
    DiffEntry *de = file_task->de;
    SeafileCrypt *crypt = data->crypt;
    gboolean no_checkout = file_task->no_checkout;
    gboolean force_conflict = file_task->force_conflict;
    const char *worktree = data->worktree;
    const char *conflict_head_id = data->conflict_head_id;
    LockedFileSet *fset = data->fset;
    HttpTxTask *http_task = data->http_task;
    SeafStat st;
    char file_id[41];
    gboolean locked_on_server = FALSE;

    if (no_checkout)
        return FETCH_CHECKOUT_SUCCESS;

    if (should_ignore_on_checkout (de->name, NULL))
        return FETCH_CHECKOUT_SUCCESS;

    rawdata_to_hex (de->sha1, file_id, 20);

    locked_on_server = seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                                             repo_id, de->name);

#if defined WIN32 || defined __APPLE__
    if (do_check_file_locked (de->name, worktree, locked_on_server)) {
        if (!locked_file_set_lookup (fset, de->name))
            send_file_sync_error_notification (repo_id, NULL, de->name,
                                               SYNC_ERROR_ID_FILE_LOCKED_BY_APP);

        locked_file_set_add_update (fset, de->name, LOCKED_OP_UPDATE,
                                    ce->ce_mtime.sec, file_id);
        /* Stay in syncing status if the file is locked. */

        return FETCH_CHECKOUT_SUCCESS;
    }
#endif

    /* Temporarily unlock the file if it's locked on server, so that the client
     * itself can write to it. 
     */
    if (locked_on_server)
        seaf_filelock_manager_unlock_wt_file (seaf->filelock_mgr,
                                              repo_id, de->name);

    /* then checkout the file. */
    gboolean conflicted = FALSE;

    CheckoutBlockAux *aux = g_new0 (CheckoutBlockAux, 1);
    aux->repo_id = g_strdup (repo_id);
    aux->host = g_strdup (http_task->host);
    aux->token = g_strdup (http_task->token);
    aux->use_fileserver_port = http_task->use_fileserver_port;
    aux->task = http_task;

    gint64 skip_buffer_size = 0;
    int block_offset = 0;
    if (!crypt)
        check_and_get_block_offset (seaf->repo_mgr, aux, repo_id, repo_version,
                                    file_id, file_task->path, &skip_buffer_size, &block_offset);
    if (skip_buffer_size > 0)
        http_task->done_download += skip_buffer_size;

    FileCheckoutData file_data = {0}; 
    int error_id = FETCH_CHECKOUT_SUCCESS;
    file_data.repo_id = repo_id;
    file_data.version = repo_version;
    file_data.file_id = file_id; 
    file_data.file_path = file_task->path;
    file_data.mode = de->mode;
    file_data.mtime = de->mtime;
    file_data.crypt = crypt;
    file_data.in_repo_path = de->name;
    file_data.conflict_head_id = conflict_head_id;
    file_data.force_conflict = force_conflict;
    file_data.conflicted = &conflicted;
    if (http_task->username)
        file_data.email = http_task->username;
    else
        file_data.email = http_task->email;
    file_data.skip_buffer_size = skip_buffer_size;
    file_data.block_offset = block_offset;

    if (seaf_fs_manager_checkout_file (seaf->fs_mgr,
                                       &file_data,
                                       &error_id,
                                       checkout_block_cb,
                                       aux) < 0) {
        seaf_warning ("Failed to checkout file %s.\n", file_task->path);

        if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                                  repo_id, de->name))
            seaf_filelock_manager_lock_wt_file (seaf->filelock_mgr,
                                                repo_id, de->name);

        free_checkout_block_aux (aux);
        return error_id;
    }
    free_checkout_block_aux (aux);

    if (locked_on_server)
        seaf_filelock_manager_lock_wt_file (seaf->filelock_mgr,
                                            repo_id, de->name);

    // cleanup_file_blocks_http (http_task, file_id);

    if (conflicted) {
        send_file_sync_error_notification (repo_id, NULL, de->name, SYNC_ERROR_ID_CONFLICT);
    } else if (!http_task->is_clone) {
        char *orig_path = NULL;
        if (check_path_conflict (de->name, &orig_path))
            send_file_sync_error_notification (repo_id, NULL, orig_path, SYNC_ERROR_ID_CONFLICT);
        g_free (orig_path);
    }

    /* finally fill cache_entry info */
    /* Only update index if we checked out the file without any error
     * or conflicts. The ctime of the entry will remain 0 if error.
     */
    seaf_stat (file_task->path, &st);
    fill_stat_cache_info (ce, &st);

    return FETCH_CHECKOUT_SUCCESS;
}

static void
fetch_file_thread_func (gpointer data, gpointer user_data)
{
    FileTxTask *task = data;
    FileTxData *tx_data = user_data;
    GAsyncQueue *finished_tasks = tx_data->finished_tasks;
    DiffEntry *de = task->de;
    char *repo_id = tx_data->repo_id;
    char file_id[41];
    gboolean is_clone = tx_data->http_task->is_clone;
    int repo_version = tx_data->repo_version;
    struct cache_entry *ce = task->ce;
    SeafileCrypt *crypt = tx_data->crypt;
    char *path = task->path;
    HttpTxTask *http_task = tx_data->http_task;
    SeafStat st;
    gboolean path_exists = FALSE;
    int rc = FETCH_CHECKOUT_SUCCESS;

    if (task->skip_fetch)
        goto out;

    rawdata_to_hex (de->sha1, file_id, 20);

    path_exists = (seaf_stat (path, &st) == 0);

    /* seaf_message ("Download file %s for repo %s\n", de->name, repo_id); */

    if (!is_clone)
        seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                              repo_id,
                                              de->name,
                                              de->mode,
                                              SYNC_STATUS_SYNCING,
                                              TRUE);

    if (path_exists && S_ISREG(st.st_mode)) {
        if (st.st_mtime == ce->ce_mtime.sec) {
            /* Worktree and index are consistent. */
            if (memcmp (de->sha1, ce->sha1, 20) == 0) {
                seaf_debug ("wt and index are consistent. no need to checkout.\n");
                task->no_checkout = TRUE;

                /* Update mode if necessary. */
                if (de->mode != ce->ce_mode) {
#ifndef WIN32
                    chmod (path, de->mode & ~S_IFMT);
                    ce->ce_mode = de->mode;
#endif
                }

                /* Update mtime if necessary. */
                if (de->mtime != ce->ce_mtime.sec) {
                    seaf_set_file_time (path, de->mtime);
                    ce->ce_mtime.sec = de->mtime;
                }

                fill_stat_cache_info (ce, &st);

                goto out;
            }
            /* otherwise we have to checkout the file. */
        } else {
            if (compare_file_content (path, &st, de->sha1, crypt, repo_version) == 0) {
                /* This happens after the worktree file was updated,
                 * but the index was not. Just need to update the index.
                 */
                seaf_debug ("update index only.\n");
                task->no_checkout = TRUE;
                fill_stat_cache_info (ce, &st);
                goto out;
            } else {
                /* Conflict. The worktree file was updated by the user. */
                seaf_message ("File %s is updated by user. "
                              "Will checkout to conflict file later.\n", path);
                task->force_conflict = TRUE;
            }
        }
    }

    /* Download the blocks of this file. */
    rc = checkout_file_http (tx_data, task);
    if (http_task->state == HTTP_TASK_STATE_CANCELED) {
        rc = FETCH_CHECKOUT_CANCELED;
        seaf_debug ("Transfer canceled.\n");
    } else if (rc == FETCH_CHECKOUT_TRANSFER_ERROR) {
        rc = FETCH_CHECKOUT_TRANSFER_ERROR;
        seaf_warning ("Transfer failed.\n");
    }

out:
    task->result = rc;
    g_async_queue_push (finished_tasks, task);
}

static gboolean
check_case_conflict (const char *path1, const char *path2, char **conflict_path)
{
    if (!path1 || !path2 || g_strcmp0 (path1, ".") == 0 || g_strcmp0 (path2, ".") == 0) {
        return FALSE;
    }
    // no case conflict
    if (strcasecmp (path1, path2) != 0) {
        return FALSE;
    }

    char *base_name1 = g_path_get_basename (path1);
    char *base_name2 = g_path_get_basename (path2);
    char *parent_dir1 = g_path_get_dirname (path1);
    char *parent_dir2 = g_path_get_dirname (path2);
    gboolean ret = FALSE;

    // case conflict
    if (strcmp (base_name1, base_name2) != 0) {
        *conflict_path = g_strdup (path2); 
        ret = TRUE;
        goto out;
    }
    // find conflict path
    ret = check_case_conflict (parent_dir1, parent_dir2, conflict_path);
out:
    g_free (base_name1);
    g_free (base_name2);
    g_free (parent_dir1);
    g_free (parent_dir2);
    return ret;
}

// Since file creation is asynchronous, the file may not have been created locally at the time of checking for case conflicts, 
// so an additional check for the name of the file being created is required.
static gboolean
is_adding_files_case_conflict (GList **adding_files, const char *name, char **conflict_path,
                               GHashTable *no_case_conflict_hash)
{
    if (g_hash_table_lookup (no_case_conflict_hash, name)) {
        return FALSE;
    }
#if defined WIN32 || defined __APPLE__
    GList *ptr;
    SeafStat st;

    ptr = *adding_files;

    char *path;
    for (; ptr; ptr = ptr->next) {
        path = ptr->data;
        if (check_case_conflict (path, name, conflict_path)){
            return TRUE;
        }
    }

    return FALSE;
#else
    return FALSE;
#endif
}

static int
schedule_file_fetch (GThreadPool *tpool,
                     const char *repo_id,
                     const char *repo_name,
                     const char *worktree,
                     struct index_state *istate,
                     DiffEntry *de,
                     const char *conflict_head_id,
                     LockedFileSet *fset,
                     GHashTable *pending_tasks,
                     GHashTable *case_conflict_hash,
                     GHashTable *no_case_conflict_hash,
                     GList **adding_files)
{
    struct cache_entry *ce;
    gboolean new_ce = FALSE;
    gboolean skip_fetch = FALSE;
    char *path = NULL;
    FileTxTask *file_task;
    gboolean no_checkout = FALSE;
    char *conflict_path = NULL;

    ce = index_name_exists (istate, de->name, strlen(de->name), 0);
    if (!ce) {
        ce = cache_entry_from_diff_entry (de);
        new_ce = TRUE;
    }

    IgnoreReason reason;
    if (should_ignore_on_checkout (de->name, &reason)) {
        seaf_message ("Path %s is invalid on Windows, skip checkout\n",
                      de->name);
        if (reason == IGNORE_REASON_END_SPACE_PERIOD)
            send_file_sync_error_notification (repo_id, repo_name, de->name,
                                               SYNC_ERROR_ID_PATH_END_SPACE_PERIOD);
        else if (reason == IGNORE_REASON_INVALID_CHARACTER)
            send_file_sync_error_notification (repo_id, repo_name, de->name,
                                               SYNC_ERROR_ID_PATH_INVALID_CHARACTER);
        skip_fetch = TRUE;
    }

    if (!skip_fetch && (is_path_case_conflict (worktree, de->name, &conflict_path, no_case_conflict_hash) ||
        is_adding_files_case_conflict(adding_files, de->name, &conflict_path, no_case_conflict_hash))) {
        if (conflict_path && !g_hash_table_lookup(case_conflict_hash, conflict_path)) {
            seaf_message ("Path %s is case conflict, skip checkout\n", conflict_path);
            send_file_sync_error_notification (repo_id, repo_name, conflict_path,
                                               SYNC_ERROR_ID_CASE_CONFLICT);
            g_hash_table_insert (case_conflict_hash, conflict_path, conflict_path);
        } else if (conflict_path) {
            g_free (conflict_path);
        }
        skip_fetch = TRUE;
        no_checkout = TRUE;
    }

    if (!skip_fetch) {
        path = build_checkout_path (worktree, de->name, strlen(de->name));
        if (!path) {
            if (new_ce)
                cache_entry_free (ce);
            return FETCH_CHECKOUT_FAILED;
        }
    }

    char *de_name = g_strdup(de->name);
    *adding_files = g_list_prepend (*adding_files, de_name);

    file_task = g_new0 (FileTxTask, 1);
    file_task->de = de;
    file_task->ce = ce;
    file_task->path = path;
    file_task->new_ce = new_ce;
    file_task->skip_fetch = skip_fetch;
    file_task->no_checkout = no_checkout;

    if (!g_hash_table_lookup (pending_tasks, de->name)) {
        g_hash_table_insert (pending_tasks, g_strdup(de->name), file_task);
        g_thread_pool_push (tpool, file_task, NULL);
    } else {
        file_tx_task_free (file_task);
    }

    return FETCH_CHECKOUT_SUCCESS;
}

static void
handle_dir_added_de (const char *repo_id,
                     const char *repo_name,
                     const char *worktree,
                     struct index_state *istate,
                     DiffEntry *de,
                     GHashTable *no_case_conflict_hash)
{
    seaf_debug ("Checkout empty dir %s.\n", de->name);

    struct cache_entry *ce;
    gboolean add_ce = FALSE;

    ce = index_name_exists (istate, de->name, strlen(de->name), 0);
    if (!ce) {
        ce = cache_entry_from_diff_entry (de);
        add_ce = TRUE;
    }

    IgnoreReason reason;
    if (should_ignore_on_checkout (de->name, &reason)) {
        seaf_message ("Path %s is invalid on Windows, skip checkout\n",
                      de->name);
        if (reason == IGNORE_REASON_END_SPACE_PERIOD)
            send_file_sync_error_notification (repo_id, repo_name, de->name,
                                               SYNC_ERROR_ID_PATH_END_SPACE_PERIOD);
        else if (reason == IGNORE_REASON_INVALID_CHARACTER)
            send_file_sync_error_notification (repo_id, repo_name, de->name,
                                               SYNC_ERROR_ID_PATH_INVALID_CHARACTER);
        goto update_index;
    }

    if (is_path_case_conflict(worktree, de->name, NULL, no_case_conflict_hash)) {
        seaf_message ("Path %s is case conflict, skip checkout\n", de->name);
        send_file_sync_error_notification (repo_id, repo_name, de->name,
                                           SYNC_ERROR_ID_CASE_CONFLICT);
        goto update_index;
    }

    checkout_empty_dir (worktree,
                        de->name,
                        de->mtime,
                        ce);

    seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                          repo_id,
                                          de->name,
                                          de->mode,
                                          SYNC_STATUS_SYNCED,
                                          TRUE);

update_index:
    if (add_ce) {
        if (!(ce->ce_flags & CE_REMOVE)) {
            add_index_entry (istate, ce,
                             (ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE));
        }
    } else
        ce->ce_mtime.sec = de->mtime;
}

#define DEFAULT_DOWNLOAD_THREADS 3

static int
download_files_http (const char *repo_id,
                     int repo_version,
                     const char *worktree,
                     struct index_state *istate,
                     const char *index_path,
                     SeafileCrypt *crypt,
                     HttpTxTask *http_task,
                     GList *results,
                     const char *conflict_head_id,
                     LockedFileSet *fset,
                     GHashTable *no_case_conflict_hash)
{
    struct cache_entry *ce;
    DiffEntry *de;
    gint64 checkout_size = 0;
    GThreadPool *tpool;
    GAsyncQueue *finished_tasks;
    GHashTable *pending_tasks;
    GHashTable *case_conflict_hash;
    GList *adding_files = NULL;
    GList *ptr;
    FileTxTask *task;
    int ret = FETCH_CHECKOUT_SUCCESS;
    gboolean checkout_file_failed = FALSE;

    finished_tasks = g_async_queue_new ();

    FileTxData data;
    memset (&data, 0, sizeof(data));
    memcpy (data.repo_id, repo_id, 36);
    data.repo_version = repo_version;
    data.crypt = crypt;
    data.http_task = http_task;
    memcpy (data.conflict_head_id, conflict_head_id, 40);
    data.finished_tasks = finished_tasks;
    data.worktree = worktree; 
    data.fset = fset;

    tpool = g_thread_pool_new (fetch_file_thread_func, &data,
                               DEFAULT_DOWNLOAD_THREADS, FALSE, NULL);

    pending_tasks = g_hash_table_new_full (g_str_hash, g_str_equal,
                                           g_free, (GDestroyNotify)file_tx_task_free);

    case_conflict_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
                                            g_free, NULL);

    for (ptr = results; ptr != NULL; ptr = ptr->next) {
        de = ptr->data;

        if (de->status == DIFF_STATUS_DIR_ADDED) {
            handle_dir_added_de (repo_id, http_task->repo_name, worktree, istate, de, no_case_conflict_hash);
        } else if (de->status == DIFF_STATUS_ADDED ||
                   de->status == DIFF_STATUS_MODIFIED) {
            if (FETCH_CHECKOUT_FAILED == schedule_file_fetch (tpool,
                                                              repo_id,
                                                              http_task->repo_name,
                                                              worktree,
                                                              istate,
                                                              de,
                                                              conflict_head_id,
                                                              fset,
                                                              pending_tasks,
                                                              case_conflict_hash,
                                                              no_case_conflict_hash,
                                                              &adding_files))
                continue;
        }
    }

    /* If there is no file need to be downloaded, return immediately. */
    if (g_hash_table_size(pending_tasks) == 0) {
        if (results != NULL)
            update_index (istate, index_path);
        goto out;
    }

    char file_id[41];
    while ((task = g_async_queue_pop (finished_tasks)) != NULL) {
        ce = task->ce;
        de = task->de;

        rawdata_to_hex (de->sha1, file_id, 20);
        /* seaf_message ("Finished downloading file %s for repo %s\n", */
        /*               de->name, repo_id); */

        if (task->result == FETCH_CHECKOUT_CANCELED ||
            task->result == FETCH_CHECKOUT_TRANSFER_ERROR) {
            ret = task->result;
            if (task->new_ce)
                cache_entry_free (task->ce);
            http_task->all_stop = TRUE;
            goto out;
        }

        int rc = task->result;

        // Record a file-level sync error when failed to checkout file.
        if (rc == FETCH_CHECKOUT_FAILED) {
            if (checkout_file_failed) {
                seaf_repo_manager_record_sync_error (repo_id, http_task->repo_name, de->name,
                                                     SYNC_ERROR_ID_CHECKOUT_FILE);
            } else {
                checkout_file_failed = TRUE;
                send_file_sync_error_notification (repo_id, http_task->repo_name, de->name,
                                                   SYNC_ERROR_ID_CHECKOUT_FILE);
            }
        }

        if (!http_task->is_clone) {
            SyncStatus status;
            if (rc == FETCH_CHECKOUT_FAILED)
                status = SYNC_STATUS_ERROR;
            else
                status = SYNC_STATUS_SYNCED;
            seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                                  repo_id,
                                                  de->name,
                                                  de->mode,
                                                  status,
                                                  TRUE);
        }

        if (task->new_ce) {
            if (!(ce->ce_flags & CE_REMOVE)) {
                add_index_entry (istate, task->ce,
                                 (ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE));
            }
        } else {
            ce->ce_mtime.sec = de->mtime;
            ce->ce_size = de->size;
            memcpy (ce->sha1, de->sha1, 20);
            if (ce->modifier) g_free (ce->modifier);
            ce->modifier = g_strdup(de->modifier);
            ce->ce_mode = create_ce_mode (de->mode);
        }

        g_hash_table_remove (pending_tasks, de->name);

        if (g_hash_table_size (pending_tasks) == 0)
            break;

        /* Save index file to disk after checking out some size of files.
         * This way we don't need to re-compare too many files if this
         * checkout is interrupted.
         */
        checkout_size += ce->ce_size;
        if (checkout_size >= UPDATE_CACHE_SIZE_LIMIT) {
            update_index (istate, index_path);
            checkout_size = 0;
        }
    }

    update_index (istate, index_path);

out:
    /* Wait until all threads exit.
     * This is necessary when the download is canceled or encountered error.
     */
    g_thread_pool_free (tpool, TRUE, TRUE);

    /* Free all pending file task structs. */
    g_hash_table_destroy (pending_tasks);

    g_hash_table_destroy (case_conflict_hash);

    if (adding_files)
        string_list_free (adding_files);

    g_async_queue_unref (finished_tasks);

    return ret;
}

static gboolean
expand_dir_added_cb (SeafFSManager *mgr,
                     const char *path,
                     SeafDirent *dent,
                     void *user_data,
                     gboolean *stop)
{
    GList **expanded = user_data;
    DiffEntry *de = NULL;
    unsigned char sha1[20];

    hex_to_rawdata (dent->id, sha1, 20);

    if (S_ISDIR(dent->mode) && strcmp(dent->id, EMPTY_SHA1) == 0)
        de = diff_entry_new (DIFF_TYPE_COMMITS, DIFF_STATUS_DIR_ADDED, sha1, path);
    else if (S_ISREG(dent->mode))
        de = diff_entry_new (DIFF_TYPE_COMMITS, DIFF_STATUS_ADDED, sha1, path);

    if (de) {
        de->mtime = dent->mtime;
        de->mode = dent->mode;
        de->modifier = g_strdup(dent->modifier);
        de->size = dent->size;
        *expanded = g_list_prepend (*expanded, de);
    }

    return TRUE;
}

/*
 * Expand DIR_ADDED results into multiple ADDED results.
 */
static int
expand_diff_results (const char *repo_id, int version,
                     const char *remote_root, const char *local_root,
                     GList **results)
{
    GList *ptr, *next;
    DiffEntry *de;
    char obj_id[41];
    GList *expanded = NULL;

    ptr = *results;
    while (ptr) {
        de = ptr->data;

        next = ptr->next;

        if (de->status == DIFF_STATUS_DIR_ADDED) {
            *results = g_list_delete_link (*results, ptr);

            rawdata_to_hex (de->sha1, obj_id, 20);
            if (seaf_fs_manager_traverse_path (seaf->fs_mgr,
                                               repo_id, version,
                                               remote_root,
                                               de->name,
                                               expand_dir_added_cb,
                                               &expanded) < 0) {
                diff_entry_free (de);
                goto error;
            }
            diff_entry_free (de);
        }

        ptr = next;
    }

    expanded = g_list_reverse (expanded);
    *results = g_list_concat (*results, expanded);

    return 0;

error:
    g_list_free_full (expanded, (GDestroyNotify)diff_entry_free);
    return -1;
}

static int
do_rename_in_worktree (DiffEntry *de, const char *worktree)
{
    char *old_path, *new_path;
    int ret = 0;

    old_path = g_build_filename (worktree, de->name, NULL);

    if (seaf_util_exists (old_path)) {
        new_path = build_checkout_path (worktree, de->new_name, strlen(de->new_name));
        if (!new_path) {
            ret = -1;
            goto out;
        }

        if (seaf_util_rename (old_path, new_path) < 0) {
            seaf_warning ("Failed to rename %s to %s: %s.\n",
                          old_path, new_path, strerror(errno));
            ret = -1;
        }

        g_free (new_path);
    }

out:
    g_free (old_path);
    return ret;
}

static gboolean
is_built_in_ignored_file (const char *filename)
{
    GPatternSpec **spec = ignore_patterns;

    while (*spec) {
        if (g_pattern_match_string(*spec, filename))
            return TRUE;
        spec++;
    }

    if (!seaf->sync_extra_temp_file) {
        spec = office_temp_ignore_patterns;
        while (*spec) {
            if (g_pattern_match_string(*spec, filename))
                return TRUE;
            spec++;
        }
    }

    return FALSE;
}

#ifdef WIN32

/*
 * @path: path relative to the worktree, utf-8 encoded
 * @path_w: absolute path include worktree, utf-16 encoded.
 * Return 0 when successfully deleted the folder; otherwise -1.
 */
static int
delete_worktree_dir_recursive_win32 (struct index_state *istate,
                                     const char *path,
                                     const wchar_t *path_w)
{
    WIN32_FIND_DATAW fdata;
    HANDLE handle;
    wchar_t *pattern;
    wchar_t *sub_path_w;
    char *sub_path, *dname;
    int path_len_w;
    DWORD error;
    int ret = 0;
    guint64 mtime;
    gboolean builtin_ignored = FALSE;

    path_len_w = wcslen(path_w);

    pattern = g_new0 (wchar_t, (path_len_w + 3));
    wcscpy (pattern, path_w);
    wcscat (pattern, L"\\*");

    handle = FindFirstFileW (pattern, &fdata);
    g_free (pattern);

    if (handle == INVALID_HANDLE_VALUE) {
        seaf_warning ("FindFirstFile failed %s: %lu.\n",
                      path, GetLastError());
        return -1;
    }

    do {
        if (wcscmp (fdata.cFileName, L".") == 0 ||
            wcscmp (fdata.cFileName, L"..") == 0)
            continue;

        dname = g_utf16_to_utf8 (fdata.cFileName, -1, NULL, NULL, NULL);
        if (!dname)
            continue;

        sub_path_w = g_new0 (wchar_t, path_len_w + wcslen(fdata.cFileName) + 2);
        wcscpy (sub_path_w, path_w);
        wcscat (sub_path_w, L"\\");
        wcscat (sub_path_w, fdata.cFileName);

        sub_path = g_strconcat (path, "/", dname, NULL);
        builtin_ignored = is_built_in_ignored_file(dname);
        g_free (dname);

        if (fdata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
            if (delete_worktree_dir_recursive_win32 (istate, sub_path, sub_path_w) < 0) {
                ret = -1;
            }
        } else {
            struct cache_entry *ce;
            /* Files like .DS_Store and Thumbs.db should be deleted any way. */
            if (!builtin_ignored) {
                mtime = (guint64)file_time_to_unix_time (&fdata.ftLastWriteTime);
                ce = index_name_exists (istate, sub_path, strlen(sub_path), 0);
                if (!ce || (!is_eml_file (dname) && ce->ce_mtime.sec != mtime)) {
                    seaf_message ("File %s is changed, skip deleting it.\n", sub_path);
                    g_free (sub_path_w);
                    g_free (sub_path);
                    ret = -1;
                    continue;
                }
            }

            if (!DeleteFileW (sub_path_w)) {
                error = GetLastError();
                seaf_warning ("Failed to delete file %s: %lu.\n",
                              sub_path, error);
                ret = -1;
            }
        }

        g_free (sub_path_w);
        g_free (sub_path);
    } while (FindNextFileW (handle, &fdata) != 0);

    error = GetLastError();
    if (error != ERROR_NO_MORE_FILES) {
        seaf_warning ("FindNextFile failed %s: %lu.\n",
                      path, error);
        ret = -1;
    }

    FindClose (handle);

    if (ret < 0)
        return ret;

    int n = 0;
    while (!RemoveDirectoryW (path_w)) {
        error = GetLastError();
        seaf_warning ("Failed to remove dir %s: %lu.\n",
                      path, error);
        if (error != ERROR_DIR_NOT_EMPTY) {
            ret = -1;
            break;
        }
        if (++n >= 3) {
            ret = -1;
            break;
        }
        /* Sleep 100ms and retry. */
        g_usleep (100000);
        seaf_warning ("Retry remove dir %s.\n", path);
    }

    return ret;
}

#else

static int
delete_worktree_dir_recursive (struct index_state *istate,
                               const char *path,
                               const char *full_path)
{
    GDir *dir;
    const char *dname;
    char *dname_nfc;
    GError *error = NULL;
    char *sub_path, *full_sub_path;
    SeafStat st;
    int ret = 0;
    gboolean builtin_ignored = FALSE;

    dir = g_dir_open (full_path, 0, &error);
    if (!dir) {
        seaf_warning ("Failed to open dir %s: %s.\n", full_path, error->message);
        return -1;
    }

    while ((dname = g_dir_read_name (dir)) != NULL) {
        dname_nfc = g_utf8_normalize (dname, -1, G_NORMALIZE_NFC);
        sub_path = g_build_path ("/", path, dname_nfc, NULL);
        full_sub_path = g_build_path ("/", full_path, dname_nfc, NULL);
        builtin_ignored = is_built_in_ignored_file (dname_nfc);
        g_free (dname_nfc);

        if (lstat (full_sub_path, &st) < 0) {
            seaf_warning ("Failed to stat %s.\n", full_sub_path);
            g_free (sub_path);
            g_free (full_sub_path);
            ret = -1;
            continue;
        }

        if (S_ISDIR(st.st_mode)) {
            if (delete_worktree_dir_recursive (istate, sub_path, full_sub_path) < 0)
                ret = -1;
        } else {
            struct cache_entry *ce;
            /* Files like .DS_Store and Thumbs.db should be deleted any way. */
            if (!builtin_ignored) {
                ce = index_name_exists (istate, sub_path, strlen(sub_path), 0);
                if (!ce || ce->ce_mtime.sec != st.st_mtime) {
                    seaf_message ("File %s is changed, skip deleting it.\n", full_sub_path);
                    g_free (sub_path);
                    g_free (full_sub_path);
                    ret = -1;
                    continue;
                }
            }

            /* Delete all other file types. */
            if (seaf_util_unlink (full_sub_path) < 0) {
                seaf_warning ("Failed to delete file %s: %s.\n",
                              full_sub_path, strerror(errno));
                ret = -1;
            }
        }

        g_free (sub_path);
        g_free (full_sub_path);
    }

    g_dir_close (dir);

    if (ret < 0)
        return ret;

    if (g_rmdir (full_path) < 0) {
        seaf_warning ("Failed to delete dir %s: %s.\n", full_path, strerror(errno));
        ret = -1;
    }

    return ret;
}

#endif  /* WIN32 */

#define SEAFILE_RECYCLE_BIN_FOLDER "recycle-bin"

static int
move_dir_to_recycle_bin (const char *dir_path)
{
    char *trash_folder = g_build_path ("/", seaf->worktree_dir, SEAFILE_RECYCLE_BIN_FOLDER, NULL);
    if (checkdir_with_mkdir (trash_folder) < 0) {
        seaf_warning ("Seafile trash folder %s doesn't exist and cannot be created.\n",
                      trash_folder);
        g_free (trash_folder);
        return -1;
    }
    g_free (trash_folder);

    char *basename = g_path_get_basename (dir_path);
    char *dst_path = g_build_path ("/", seaf->worktree_dir, SEAFILE_RECYCLE_BIN_FOLDER, basename, NULL);
    int ret = 0;

    int n;
    char *tmp_path;
    for (n = 1; n < 10; ++n) {
        if (g_file_test (dst_path, G_FILE_TEST_EXISTS)) {
            tmp_path = g_strdup_printf ("%s(%d)", dst_path, n);
            g_free (dst_path);
            dst_path = tmp_path;
            continue;
        }
        break;
    }

    if (seaf_util_rename (dir_path, dst_path) < 0) {
        seaf_warning ("Failed to move %s to Seafile recycle bin %s: %s\n",
                      dir_path, dst_path, strerror(errno));
        ret = -1;
        goto out;
    }

    seaf_message ("Moved folder %s to Seafile recycle bin %s.\n",
                  dir_path, dst_path);

out:
    g_free (basename);
    g_free (dst_path);
    return ret;
}

static void
delete_worktree_dir (const char *repo_id,
                     const char *repo_name,
                     struct index_state *istate,
                     const char *worktree,
                     const char *path)
{
    char *full_path = g_build_path ("/", worktree, path, NULL);

#ifdef WIN32
    wchar_t *full_path_w = win32_long_path (full_path);
    delete_worktree_dir_recursive_win32 (istate, path, full_path_w);
    g_free (full_path_w);
#else
    delete_worktree_dir_recursive(istate, path, full_path);
#endif

    /* If for some reason the dir cannot be removed, try to move it to a trash folder
     * under Seafile folder. Otherwise the removed folder will be created agian on the
     * server, which will confuse the users.
     */
    if (g_file_test (full_path, G_FILE_TEST_EXISTS)) {
        if (move_dir_to_recycle_bin (full_path) == 0)
            send_file_sync_error_notification (repo_id, repo_name, path,
                                               SYNC_ERROR_ID_REMOVE_UNCOMMITTED_FOLDER);
    }

    g_free (full_path);
}

static void
update_sync_status (struct cache_entry *ce, void *user_data)
{
    char *repo_id = user_data;

    seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                          repo_id,
                                          ce->name,
                                          ce->ce_mode,
                                          SYNC_STATUS_SYNCED,
                                          TRUE);
}

static int
convert_rename_to_checkout (const char *repo_id,
                            int repo_version,
                            const char *root_id,
                            DiffEntry *de,
                            GList **entries)
{
    if (de->status == DIFF_STATUS_RENAMED) {
        char file_id[41];
        SeafDirent *dent = NULL;
        DiffEntry *new_de = NULL;

        rawdata_to_hex (de->sha1, file_id, 20);
        dent = seaf_fs_manager_get_dirent_by_path (seaf->fs_mgr,
                                                   repo_id,
                                                   repo_version,
                                                   root_id,
                                                   de->new_name,
                                                   NULL);
        if (!dent) {
            seaf_warning ("Failed to find %s in repo %s\n",
                          de->new_name, repo_id);
            return -1;
        }

        new_de = diff_entry_new (DIFF_TYPE_COMMITS, DIFF_STATUS_ADDED,
                                 de->sha1, de->new_name);
        if (new_de) {
            new_de->mtime = dent->mtime;
            new_de->mode = dent->mode;
            new_de->modifier = g_strdup(dent->modifier);
            new_de->size = dent->size;
            *entries = g_list_prepend (*entries, new_de);
        }

        seaf_dirent_free (dent);
    } else if (de->status == DIFF_STATUS_DIR_RENAMED) {
        GList *expanded = NULL;

        if (seaf_fs_manager_traverse_path (seaf->fs_mgr,
                                           repo_id, repo_version,
                                           root_id,
                                           de->new_name,
                                           expand_dir_added_cb,
                                           &expanded) < 0) {
            g_list_free_full (expanded, (GDestroyNotify)diff_entry_free);
            return -1;
        }

        *entries = g_list_concat (*entries, expanded);
    }

    return 0;
}

static gboolean
load_enc_keys_cb (sqlite3_stmt *stmt, void *vcrypt)
{
    SeafileCrypt *crypt = vcrypt;
    const char *key, *iv;

    key = (const char *)sqlite3_column_text(stmt, 0);
    iv = (const char *)sqlite3_column_text(stmt, 1);

    if (crypt->version == 1) {
        hex_to_rawdata (key, crypt->key, 16);
        hex_to_rawdata (iv, crypt->iv, 16);
    } else if (crypt->version >= 2) {
        hex_to_rawdata (key, crypt->key, 32);
        hex_to_rawdata (iv, crypt->iv, 16);
    }

    return FALSE;
}

static int
load_crypt_from_enc_info (SeafRepoManager *manager, const char *repo_id, SeafileCrypt *crypt)
{
    sqlite3 *db = manager->priv->db;
    char sql[256];
    int n;

    pthread_mutex_lock (&manager->priv->db_lock);

    snprintf (sql, sizeof(sql), 
              "SELECT key, iv FROM RepoKeys WHERE repo_id='%s'",
              repo_id);
    n = sqlite_foreach_selected_row (db, sql, load_enc_keys_cb, crypt);
    if (n < 0) {
        pthread_mutex_unlock (&manager->priv->db_lock);
        return -1;
    }

    pthread_mutex_unlock (&manager->priv->db_lock);

    return 0;
}

static gboolean
handle_de_rename (HttpTxTask *http_task, const char *worktree, DiffEntry *de,
                  struct index_state *istate, GHashTable *no_case_conflict_hash)
{
    char *repo_id = http_task->repo_id;
    int repo_version = http_task->repo_version;
    gboolean is_clone = http_task->is_clone;
    struct cache_entry *ce;

#if defined WIN32 || defined __APPLE__
    gboolean old_path_conflict = is_path_case_conflict(worktree, de->name, NULL, no_case_conflict_hash);
    gboolean new_path_conflict = is_path_case_conflict(worktree, de->new_name, NULL, no_case_conflict_hash);

    if (g_strcasecmp (de->name, de->new_name) == 0) {
        if (!old_path_conflict && !new_path_conflict) {
            return TRUE;
        } else if (old_path_conflict && !new_path_conflict) {
            seaf_message ("Path %s is renamed to %s, which has case conflict and will not be checked out.\n", de->name, de->new_name);
            send_file_sync_error_notification (repo_id, NULL, de->new_name,
                           SYNC_ERROR_ID_CASE_CONFLICT);
            return FALSE;
        } else if (!old_path_conflict && new_path_conflict) {
            do_rename_in_worktree (de, worktree);
        } else {
            seaf_message ("Path %s is renamed to %s, which has case conflict and will not be checked out.\n", de->name, de->new_name);
            send_file_sync_error_notification (repo_id, NULL, de->new_name,
                           SYNC_ERROR_ID_CASE_CONFLICT);
            return FALSE;
        }
    } else {
        if (!old_path_conflict && !new_path_conflict) {
            do_rename_in_worktree (de, worktree);
        } else if (old_path_conflict && !new_path_conflict) {
            seaf_message ("Case conflict path %s is renamed to %s without case conflict, check it out\n", de->name, de->new_name);
            return TRUE;
        } else if (!old_path_conflict && new_path_conflict) {
            // check if file has been changed and delete old path.
            if (de->status == DIFF_STATUS_DIR_RENAMED) {
                seaf_message ("Path %s is renamed to %s, which has case conflict and will not be checked out. Delete it\n", de->name, de->new_name);
                send_file_sync_error_notification (repo_id, NULL, de->new_name,
                               SYNC_ERROR_ID_CASE_CONFLICT);
                delete_worktree_dir (repo_id, http_task->repo_name, istate, worktree, de->name);
            } else {
                ce = index_name_exists (istate, de->name, strlen(de->name), 0);
                if (ce) {
                    seaf_message ("Path %s is renamed to %s, which has case conflict and will not be checked out. Delete it\n", de->name, de->new_name);
                    send_file_sync_error_notification (repo_id, NULL, de->new_name,
                                   SYNC_ERROR_ID_CASE_CONFLICT);
                    delete_path (worktree, de->name, de->mode, ce->ce_mtime.sec);
                }
            }
        } else {
            seaf_message ("Path %s is renamed to %s, which has case conflict and will not be checked out.\n", de->name, de->new_name);
            send_file_sync_error_notification (repo_id, NULL, de->new_name,
                           SYNC_ERROR_ID_CASE_CONFLICT);
            return FALSE;
        }
    }
#else
    do_rename_in_worktree (de, worktree);
#endif

    return FALSE;
}

int
seaf_repo_fetch_and_checkout (HttpTxTask *http_task, const char *remote_head_id)
{
    char *repo_id;
    int repo_version;
    gboolean is_clone;
    char *worktree;
    char *passwd;

    SeafRepo *repo = NULL;
    SeafBranch *master = NULL;
    SeafCommit *remote_head = NULL, *master_head = NULL;
    char index_path[SEAF_PATH_MAX];
    struct index_state istate;
    int ret = FETCH_CHECKOUT_SUCCESS;
    GList *results = NULL;
    SeafileCrypt *crypt = NULL;
    GList *ignore_list = NULL;
    LockedFileSet *fset = NULL;
    GHashTable *no_case_conflict_hash = NULL;

    repo_id = http_task->repo_id;
    repo_version = http_task->repo_version;
    is_clone = http_task->is_clone;
    worktree = http_task->worktree;
    passwd = http_task->passwd;

    memset (&istate, 0, sizeof(istate));
    snprintf (index_path, SEAF_PATH_MAX, "%s/%s",
              seaf->repo_mgr->index_dir, repo_id);
    if (read_index_from (&istate, index_path, repo_version) < 0) {
        seaf_warning ("Failed to load index.\n");
        return FETCH_CHECKOUT_FAILED;
    }

    no_case_conflict_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
                                            g_free, NULL);

    if (!is_clone) {
        repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id);
        if (!repo) {
            seaf_warning ("Failed to get repo %.8s.\n", repo_id);
            goto out;
        }

        master = seaf_branch_manager_get_branch (seaf->branch_mgr,
                                                 repo_id, "master");
        if (!master) {
            seaf_warning ("Failed to get master branch for repo %.8s.\n",
                          repo_id);
            ret = FETCH_CHECKOUT_FAILED;
            goto out;
        }

        master_head = seaf_commit_manager_get_commit (seaf->commit_mgr,
                                                      repo_id,
                                                      repo_version,
                                                      master->commit_id);
        if (!master_head) {
            seaf_warning ("Failed to get master head %s of repo %.8s.\n",
                          repo_id, master->commit_id);
            ret = FETCH_CHECKOUT_FAILED;
            goto out;
        }
    }

    if (!is_clone)
        worktree = repo->worktree;

    remote_head = seaf_commit_manager_get_commit (seaf->commit_mgr,
                                                  repo_id,
                                                  repo_version,
                                                  remote_head_id);
    if (!remote_head) {
        seaf_warning ("Failed to get remote head %s of repo %.8s.\n",
                      repo_id, remote_head_id);
        ret = FETCH_CHECKOUT_FAILED;
        goto out;
    }

    if (diff_commit_roots (repo_id, repo_version,
                           master_head ? master_head->root_id : EMPTY_SHA1,
                           remote_head->root_id,
                           &results, TRUE) < 0) {
        seaf_warning ("Failed to diff for repo %.8s.\n", repo_id);
        ret = FETCH_CHECKOUT_FAILED;
        goto out;
    }

    GList *ptr;
    DiffEntry *de;

    /* Expand DIR_ADDED diff entries. */
    if (expand_diff_results (repo_id, repo_version,
                             remote_head->root_id,
                             master_head ? master_head->root_id : EMPTY_SHA1,
                             &results) < 0) {
        ret = FETCH_CHECKOUT_FAILED;
        goto out;
    }

#ifdef WIN32
    for (ptr = results; ptr; ptr = ptr->next) {
        de = ptr->data;
        if (de->status == DIFF_STATUS_DIR_RENAMED ||
            de->status == DIFF_STATUS_DIR_DELETED) {
            if (do_check_dir_locked (de->name, worktree)) {
                seaf_message ("File(s) in dir %s are locked by other program, "
                              "skip rename/delete.\n", de->name);
                send_file_sync_error_notification (repo_id, NULL, de->name,
                                                   SYNC_ERROR_ID_FOLDER_LOCKED_BY_APP);
                ret = FETCH_CHECKOUT_LOCKED;
                goto out;
            }
        } else if (de->status == DIFF_STATUS_RENAMED) {
            gboolean locked_on_server = seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                                                              repo_id,
                                                                              de->name);

            if (do_check_file_locked (de->name, worktree, locked_on_server)) {
                seaf_message ("File %s is locked by other program, skip rename.\n",
                              de->name);
                send_file_sync_error_notification (repo_id, NULL, de->name,
                                                   SYNC_ERROR_ID_FILE_LOCKED_BY_APP);
                ret = FETCH_CHECKOUT_LOCKED;
                goto out;
            }
        }
    }
#endif

    if (remote_head->encrypted) {
        if (!is_clone) {
            crypt = seafile_crypt_new (repo->enc_version,
                                       repo->enc_key,
                                       repo->enc_iv);
        } else if (passwd){
            unsigned char enc_key[32], enc_iv[16];
            if (seafile_decrypt_repo_enc_key (remote_head->enc_version,
                                          passwd,
                                          remote_head->random_key,
                                          remote_head->salt,
                                          enc_key, enc_iv) < 0) {
                seaf_warning ("Failed to decrypt repo enc key: %s\n", repo_id);
                ret = FETCH_CHECKOUT_FAILED;
                goto out;
            }
            crypt = seafile_crypt_new (remote_head->enc_version,
                                       enc_key, enc_iv);
        } else {
            // resync an encrypted repo, get key and iv from db. 
            crypt = g_new0 (SeafileCrypt, 1);
            crypt->version = remote_head->enc_version;
            load_crypt_from_enc_info (seaf->repo_mgr, repo_id, crypt);
        }
    }

    ignore_list = seaf_repo_load_ignore_files (worktree);

    struct cache_entry *ce;

#if defined WIN32 || defined __APPLE__
    fset = seaf_repo_manager_get_locked_file_set (seaf->repo_mgr, repo_id);
#endif

    for (ptr = results; ptr; ptr = ptr->next) {
        de = ptr->data;
        if (de->status == DIFF_STATUS_DELETED) {
            seaf_debug ("Delete file %s.\n", de->name);

            ce = index_name_exists (&istate, de->name, strlen(de->name), 0);
            if (!ce)
                continue;

            if (should_ignore_on_checkout (de->name, NULL)) {
                remove_from_index_with_prefix (&istate, de->name, NULL);
                try_add_empty_parent_dir_entry (worktree, &istate, de->name);
                continue;
            }

            gboolean locked_on_server = seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                                                              repo_id,
                                                                              de->name);
            if (locked_on_server)
                seaf_filelock_manager_unlock_wt_file (seaf->filelock_mgr,
                                                      repo_id, de->name);

#if defined WIN32 || defined __APPLE__
            if (!do_check_file_locked (de->name, worktree, locked_on_server)) {
                locked_file_set_remove (fset, de->name, FALSE);
                if (!is_path_case_conflict (worktree, de->name, NULL, no_case_conflict_hash)) {
                    delete_path (worktree, de->name, de->mode, ce->ce_mtime.sec);
                } else {
                    seaf_message ("Path %s is case conflict, skip delete\n", de->name);
                    send_file_sync_error_notification (repo_id, NULL, de->name,
                                                       SYNC_ERROR_ID_CASE_CONFLICT);
                }
            } else {
                if (!locked_file_set_lookup (fset, de->name))
                    send_file_sync_error_notification (repo_id, http_task->repo_name, de->name,
                                                       SYNC_ERROR_ID_FILE_LOCKED_BY_APP);

                locked_file_set_add_update (fset, de->name, LOCKED_OP_DELETE,
                                            ce->ce_mtime.sec, NULL);
            }
#else
            delete_path (worktree, de->name, de->mode, ce->ce_mtime.sec);
#endif

            /* No need to lock wt file again since it's deleted. */

            remove_from_index_with_prefix (&istate, de->name, NULL);
            try_add_empty_parent_dir_entry (worktree, &istate, de->name);
        } else if (de->status == DIFF_STATUS_DIR_DELETED) {
            seaf_debug ("Delete dir %s.\n", de->name);

            /* Nothing to delete. */
            if (!master_head || strcmp(master_head->root_id, EMPTY_SHA1) == 0)
                continue;

            if (should_ignore_on_checkout (de->name, NULL)) {
                seaf_message ("Path %s is invalid on Windows, skip delete.\n",
                              de->name);
                remove_from_index_with_prefix (&istate, de->name, NULL);

                try_add_empty_parent_dir_entry (worktree, &istate, de->name);
                continue;
            }

            if (!is_path_case_conflict (worktree, de->name, NULL, no_case_conflict_hash)) {
                delete_worktree_dir (repo_id, http_task->repo_name, &istate, worktree, de->name);
            } else {
                seaf_message ("Path %s is case conflict, skip delete\n", de->name);
                send_file_sync_error_notification (repo_id, NULL, de->name,
                                                   SYNC_ERROR_ID_CASE_CONFLICT);
            }

            /* Remove all index entries under this directory */
            remove_from_index_with_prefix (&istate, de->name, NULL);

            try_add_empty_parent_dir_entry (worktree, &istate, de->name);
        }
    }

    for (ptr = results; ptr; ptr = ptr->next) {
        de = ptr->data;
        if (de->status == DIFF_STATUS_RENAMED ||
            de->status == DIFF_STATUS_DIR_RENAMED) {
            seaf_debug ("Rename %s to %s.\n", de->name, de->new_name);

#ifdef WIN32
            IgnoreReason reason;
            if (should_ignore_on_checkout (de->new_name, &reason)) {
                seaf_message ("Path %s is invalid on Windows, skip rename.\n", de->new_name);

                if (reason == IGNORE_REASON_END_SPACE_PERIOD)
                    send_file_sync_error_notification (repo_id, http_task->repo_name,
                                                       de->new_name,
                                                       SYNC_ERROR_ID_PATH_END_SPACE_PERIOD);
                else if (reason == IGNORE_REASON_INVALID_CHARACTER)
                    send_file_sync_error_notification (repo_id, http_task->repo_name,
                                                       de->new_name,
                                                       SYNC_ERROR_ID_PATH_INVALID_CHARACTER);
                continue;
            } else if (should_ignore_on_checkout (de->name, NULL)) {
                /* If the server renames an invalid path to a valid path,
                 * directly checkout the valid path. The checkout will merge
                 * with any existing files.
                 */
                convert_rename_to_checkout (repo_id, repo_version,
                                            remote_head->root_id,
                                            de, &results);
                continue;
            }
#endif

            if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                                      repo_id, de->name))
                seaf_filelock_manager_unlock_wt_file (seaf->filelock_mgr,
                                                      repo_id, de->name);

            gboolean checkout_directly = handle_de_rename (http_task, worktree, de, &istate, no_case_conflict_hash);
            if (checkout_directly) {
                convert_rename_to_checkout (repo_id, repo_version,
                                            remote_head->root_id,
                                            de, &results);
            } else {
                /* update_sync_status updates the sync status for each renamed path.
                 * The renamed file/folder becomes "synced" immediately after rename.
                 */
                if (!is_clone)
                    rename_index_entries (&istate, de->name, de->new_name, NULL,
                                          update_sync_status, repo_id);
                else
                    rename_index_entries (&istate, de->name, de->new_name, NULL,
                                          NULL, NULL);

                /* Moving files out of a dir may make it empty. */
                try_add_empty_parent_dir_entry (worktree, &istate, de->name);
            }
        }
    }

    if (istate.cache_changed)
        update_index (&istate, index_path);

    for (ptr = results; ptr; ptr = ptr->next) {
        de = ptr->data;
        if (de->status == DIFF_STATUS_ADDED || de->status == DIFF_STATUS_MODIFIED) {
            http_task->total_download += de->size;
        }
    }

    ret = download_files_http (repo_id,
                               repo_version,
                               worktree,
                               &istate,
                               index_path,
                               crypt,
                               http_task,
                               results,
                               remote_head_id,
                               fset,
                               no_case_conflict_hash);

out:
    discard_index (&istate);

    seaf_branch_unref (master);
    seaf_commit_unref (master_head);
    seaf_commit_unref (remote_head);

    g_list_free_full (results, (GDestroyNotify)diff_entry_free);

    g_free (crypt);

    if (ignore_list)
        seaf_repo_free_ignore_files (ignore_list);

#if defined WIN32 || defined __APPLE__
    locked_file_set_free (fset);
#endif

    g_hash_table_destroy (no_case_conflict_hash);

    return ret;
}

int
seaf_repo_manager_set_repo_worktree (SeafRepoManager *mgr,
                                     SeafRepo *repo,
                                     const char *worktree)
{
    if (g_access(worktree, F_OK) != 0)
        return -1;

    if (repo->worktree)
        g_free (repo->worktree);
    repo->worktree = g_strdup(worktree);

    if (seaf_repo_manager_set_repo_property (mgr, repo->id,
                                             "worktree",
                                             repo->worktree) < 0)
        return -1;

    repo->worktree_invalid = FALSE;

    return 0;
}

void
seaf_repo_manager_invalidate_repo_worktree (SeafRepoManager *mgr,
                                            SeafRepo *repo)
{
    if (repo->worktree_invalid)
        return;

    repo->worktree_invalid = TRUE;

    if (repo->auto_sync && (repo->sync_interval == 0)) {
        if (seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id) < 0) {
            seaf_warning ("failed to unwatch repo %s.\n", repo->id);
        }
    }
}

void
seaf_repo_manager_validate_repo_worktree (SeafRepoManager *mgr,
                                          SeafRepo *repo)
{
    if (!repo->worktree_invalid)
        return;

    repo->worktree_invalid = FALSE;

    if (repo->auto_sync && (repo->sync_interval == 0)) {
        if (seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree) < 0) {
            seaf_warning ("failed to watch repo %s.\n", repo->id);
        }
    }
}

SeafRepoManager*
seaf_repo_manager_new (SeafileSession *seaf)
{
    SeafRepoManager *mgr = g_new0 (SeafRepoManager, 1);

    mgr->priv = g_new0 (SeafRepoManagerPriv, 1);
    mgr->seaf = seaf;
    mgr->index_dir = g_build_path (PATH_SEPERATOR, seaf->seaf_dir, INDEX_DIR, NULL);

    pthread_mutex_init (&mgr->priv->db_lock, NULL);

    mgr->priv->checkout_tasks_hash = g_hash_table_new_full
        (g_str_hash, g_str_equal, g_free, g_free);

    ignore_patterns = g_new0 (GPatternSpec*, G_N_ELEMENTS(ignore_table));
    int i;
    for (i = 0; ignore_table[i] != NULL; i++) {
        ignore_patterns[i] = g_pattern_spec_new (ignore_table[i]);
    }

    office_temp_ignore_patterns[0] = g_pattern_spec_new("~$*");
    /* for files like ~WRL0001.tmp for docx and *.tmp for xlsx and pptx */
    office_temp_ignore_patterns[1] = g_pattern_spec_new("*.tmp");
    office_temp_ignore_patterns[2] = g_pattern_spec_new(".~lock*#");
    /* for temporary files of WPS Office */
    office_temp_ignore_patterns[3] = g_pattern_spec_new(".~*");
    office_temp_ignore_patterns[4] = NULL;

    GError *error = NULL;
    conflict_pattern = g_regex_new (CONFLICT_PATTERN, 0, 0, &error);
    if (error) {
        seaf_warning ("Failed to create regex '%s': %s\n",
                      CONFLICT_PATTERN, error->message);
        g_clear_error (&error);
    }

    office_lock_pattern = g_regex_new (OFFICE_LOCK_PATTERN, 0, 0, &error);
    if (error) {
        seaf_warning ("Failed to create regex '%s': %s\n",
                      OFFICE_LOCK_PATTERN, error->message);
        g_clear_error (&error);
    }
    libre_office_lock_pattern = g_regex_new (LIBRE_OFFICE_LOCK_PATTERN, 0, 0, &error);
    if (error) {
        seaf_warning ("Failed to create libre office regex '%s': %s\n",
                      LIBRE_OFFICE_LOCK_PATTERN, error->message);
        g_clear_error (&error);
    }
    wps_lock_pattern = g_regex_new (WPS_LOCK_PATTERN, 0, 0, &error);
    if (error) {
        seaf_warning ("Failed to create wps regex '%s': %s\n",
                      WPS_LOCK_PATTERN, error->message);
        g_clear_error (&error);
    }

    mgr->priv->repo_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

    pthread_rwlock_init (&mgr->priv->lock, NULL);

    mgr->priv->lock_office_job_queue = g_async_queue_new ();

    pthread_mutex_init (&mgr->priv->errors_lock, NULL);

    mgr->priv->block_map_cache_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
    pthread_rwlock_init (&mgr->priv->block_map_lock, NULL);

    mgr->priv->user_perms = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
    mgr->priv->group_perms = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
    pthread_mutex_init (&mgr->priv->perm_lock, NULL);

    return mgr;
}

int
seaf_repo_manager_init (SeafRepoManager *mgr)
{
    if (checkdir_with_mkdir (mgr->index_dir) < 0) {
        seaf_warning ("Index dir %s does not exist and is unable to create\n",
                   mgr->index_dir);
        return -1;
    }

    /* Load all the repos into memory on the client side. */
    load_repos (mgr, mgr->seaf->seaf_dir);

    /* Load folder permissions from db. */
    init_folder_perms (mgr);

    return 0;
}

static void
watch_repos (SeafRepoManager *mgr)
{
    GHashTableIter iter;
    SeafRepo *repo;
    gpointer key, value;

    g_hash_table_iter_init (&iter, mgr->priv->repo_hash);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        repo = value;
        if (repo->auto_sync && !repo->worktree_invalid && (repo->sync_interval == 0)) {
            if (seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree) < 0) {
                seaf_warning ("failed to watch repo %s.\n", repo->id);
                /* If we fail to add watch at the beginning, sync manager
                 * will periodically check repo status and retry.
                 */
            }
        }
    }
}

#define REMOVE_OBJECTS_BATCH 1000

static int
remove_store (const char *top_store_dir, const char *store_id, int *count)
{
    char *obj_dir = NULL;
    GDir *dir1, *dir2;
    const char *dname1, *dname2;
    char *path1, *path2;

    obj_dir = g_build_filename (top_store_dir, store_id, NULL);

    dir1 = g_dir_open (obj_dir, 0, NULL);
    if (!dir1) {
        g_free (obj_dir);
        return 0;
    }

    seaf_message ("Removing store %s\n", obj_dir);

    while ((dname1 = g_dir_read_name(dir1)) != NULL) {
        path1 = g_build_filename (obj_dir, dname1, NULL);

        dir2 = g_dir_open (path1, 0, NULL);
        if (!dir2) {
            seaf_warning ("Failed to open obj dir %s.\n", path1);
            g_dir_close (dir1);
            g_free (path1);
            g_free (obj_dir);
            return -1;
        }

        while ((dname2 = g_dir_read_name(dir2)) != NULL) {
            path2 = g_build_filename (path1, dname2, NULL);
            g_unlink (path2);

            /* To prevent using too much IO, only remove 1000 objects per 5 seconds.
             */
            if (++(*count) > REMOVE_OBJECTS_BATCH) {
                g_usleep (5 * G_USEC_PER_SEC);
                *count = 0;
            }

            g_free (path2);
        }
        g_dir_close (dir2);

        g_rmdir (path1);
        g_free (path1);
    }

    g_dir_close (dir1);
    g_rmdir (obj_dir);
    g_free (obj_dir);

    return 0;
}

static void
cleanup_deleted_stores_by_type (const char *type)
{
    char *top_store_dir;
    const char *repo_id;

    top_store_dir = g_build_filename (seaf->seaf_dir, "deleted_store", type, NULL);

    GError *error = NULL;
    GDir *dir = g_dir_open (top_store_dir, 0, &error);
    if (!dir) {
        seaf_warning ("Failed to open store dir %s: %s.\n",
                      top_store_dir, error->message);
        g_free (top_store_dir);
        return;
    }

    int count = 0;
    while ((repo_id = g_dir_read_name(dir)) != NULL) {
        remove_store (top_store_dir, repo_id, &count);
    }

    g_free (top_store_dir);
    g_dir_close (dir);
}

static void *
cleanup_deleted_stores (void *vdata)
{
    while (1) {
        cleanup_deleted_stores_by_type ("commits");
        cleanup_deleted_stores_by_type ("fs");
        cleanup_deleted_stores_by_type ("blocks");
        g_usleep (60 * G_USEC_PER_SEC);
    }
    return NULL;
}

struct _GetBlockAux {
    char *content;
    int size;
};
typedef struct _GetBlockAux GetBlockAux;

static size_t
get_enc_block_cb (void *contents, size_t size, size_t nmemb, void *userp)
{
    size_t realsize = size * nmemb;
    GetBlockAux *aux = userp;
    int ret = realsize;

    aux->content = g_realloc (aux->content, aux->size + realsize);
    if (!aux->content) {
        seaf_warning ("Not enough memory.\n");
        return 0;
    }
    memcpy (aux->content + aux->size, contents, realsize);
    aux->size += realsize;

    return ret;
}

static int
decrypt_block (SeafRepo *repo, SeafileCrypt *crypt, SeafileCrypt *zero_crypt, const char *block_id)
{
    int ret = 0;
    char *dec_out = NULL;
    int dec_out_len = -1;
    GetBlockAux aux;
    memset (&aux, 0, sizeof(aux));

    int error_id;
    if (http_tx_manager_get_block(seaf->http_tx_mgr, repo->id,
                                  block_id, repo->effective_host,
                                  repo->token, repo->use_fileserver_port,
                                  &error_id,
                                  get_enc_block_cb, &aux) < 0) {
        seaf_warning ("Failed to get block %s from server.\n",
                      block_id);
        goto out;
    }

    int rc = seafile_decrypt (&dec_out, &dec_out_len, aux.content, aux.size, crypt);
    if (rc != 0) {
        rc = seafile_decrypt(&dec_out, &dec_out_len, aux.content, aux.size, zero_crypt);
        if (rc == 0) {
            ret = -1;
            goto out;
        }
    }


out:
    g_free (dec_out);
    g_free (aux.content);
    return ret;
}

static int
check_repo_corrupted_blocks (SeafRepo *repo)
{
    SeafCommit *head = NULL;
    SeafileCrypt *crypt = NULL;
    SeafileCrypt *zero_crypt = NULL;
    char path[SEAF_PATH_MAX];
    char index_path[SEAF_PATH_MAX];
    struct index_state istate;
    gboolean not_exist;
    int ret = 0;

    memset (&istate, 0, sizeof(istate));
    snprintf (index_path, SEAF_PATH_MAX, "%s/%s", repo->manager->index_dir, repo->id);

    if (read_index_from (&istate, index_path, repo->version) < 0) {
        seaf_warning ("Failed to load repo index: %s.\n", repo->id);
        return -1;
    }

    head = seaf_commit_manager_get_commit (seaf->commit_mgr,
                                           repo->id, repo->version,
                                           repo->head->commit_id);
    if (!head) {
        ret = -1;
        seaf_warning ("Failed to get head for repo %s.\n", repo->id);
        goto out;
    }

    crypt = seafile_crypt_new (repo->enc_version, repo->enc_key, repo->enc_iv);
    unsigned char enc_key[32], enc_iv[16];
    memset (enc_key, 0, sizeof(enc_key)); 
    memset (enc_iv, 0, sizeof(enc_iv)); 
    zero_crypt = seafile_crypt_new (repo->enc_version, enc_key, enc_iv);

    struct cache_entry **ce_array = istate.cache;
    struct cache_entry *ce;
    SeafStat st;
    unsigned char sha1[20];
    int i;

    for (i = 0; i < istate.cache_nr; ++i) {
        ce = ce_array[i];
        if (S_ISDIR (ce->ce_mode)) {
            continue;
        }

        snprintf (path, SEAF_PATH_MAX, "%s/%s", repo->worktree, ce->name);
        not_exist = FALSE;
        int rc = seaf_stat (path, &st);
        if (rc < 0 && errno == ENOENT) {
            not_exist = TRUE;
        } else if (rc == 0 && st.st_size == 0) {
            continue;
        }

        GError *error = NULL;
        char *file_id = seaf_fs_manager_get_seafile_id_by_path (seaf->fs_mgr, repo->id, repo->version, head->root_id, ce->name, &error);
        if (error) {
            g_clear_error (&error);
        }
        if (!file_id) {
            seaf_warning ("Failed to get seafile id by path: %s\n", path);
            continue;
        }
        // When a local file exists, the client checks whether the file is correctly encrypted by reindexing it and comparing the fileID.
        // Since files can be uploaded either via the web or the client, if the fileID generated using the CDC algorithm does not match, the client will perform a second check using a non-CDC algorithm.
        // If the fileID still does not match, the file is considered to have corrupted encryption.

        // If the local file does not exist, it may have failed to checkout due to decryption errors. In this case, the client attempts to retrieve the file blocks from the server and decrypt them.       // If the file can be decrypted using an all-zero key, it is considered to have corrupted encryption.
        if (!not_exist) {
            char new_file_id[41];
            gint64 size;
            // Reindex file blocks using cdc.
            if (seaf_fs_manager_index_blocks (seaf->fs_mgr, repo->id, repo->version, path, sha1, &size, crypt, TRUE, TRUE) < 0) {
                g_free (file_id);
                seaf_warning ("Failed to index file: %s\n", path);
                continue;
            }
            rawdata_to_hex (sha1, new_file_id, 20);
            if (g_strcmp0 (file_id, new_file_id) == 0) {
                g_free (file_id);
                continue;
            }

            // Reindex file block without using cdc.
            if (seaf_fs_manager_index_blocks (seaf->fs_mgr, repo->id, repo->version, path, sha1, &size, crypt, TRUE, FALSE) < 0) {
                g_free (file_id);
                seaf_warning ("Failed to index file: %s\n", path);
                continue;
            }
            rawdata_to_hex (sha1, new_file_id, 20);
            if (g_strcmp0 (file_id, new_file_id) == 0) {
                g_free (file_id);
                continue;
            }
            g_free (file_id);
            if (ce->ce_mtime.sec != st.st_mtime) {
                continue;
            }
            seaf_warning ("Failed to compare file id for path %s, blocks are corrupted.\n", path);
            repo->empty_enc_key = TRUE;
            save_repo_property (seaf->repo_mgr, repo->id, REPO_PROP_EMPTY_ENC_KEY, "true");
            break;
        } else if (ret == 0 && (ce->ce_ctime.sec == 0 && ce_stage(ce) == 0)) {
            // If ce->ctime is 0 and stage is 0, it was not successfully checked out.
            Seafile *seafile = seaf_fs_manager_get_seafile (seaf->fs_mgr, repo->id, repo->version, file_id);
            if (!seafile) {
                g_free (file_id);
                seaf_warning ("Failed to find file %s in repo %s\n", file_id, repo->id);
                continue;
            }
            if (seafile->n_blocks == 0) {
                g_free (file_id);
                continue;
            }
            char *block_id = seafile->blk_sha1s[0];
            if (decrypt_block (repo, crypt, zero_crypt, block_id) < 0) {
                g_free (file_id);
                seaf_warning ("Failed to compare block_id %s, block is corrupted.\n", block_id);
                repo->empty_enc_key = TRUE;
                save_repo_property (seaf->repo_mgr, repo->id, REPO_PROP_EMPTY_ENC_KEY, "true");
                break;

            }
            g_free (file_id);
        }
    }


out:
    discard_index (&istate);
    seaf_commit_unref (head);
    g_free (crypt);
    g_free (zero_crypt);
    return ret;
}

static char *
load_repo_property (SeafRepoManager *manager,
                    const char *repo_id,
                    const char *key);

// The older versions of the client may fail to import the encryption key due to missing records in RepoKeys or database errors.
// As a result, some files might be encrypted using an all-zero key.
// This check will reindex the local files to determine whether any of files were encrypted with an incorrect key.
static void *
check_corrupted_enc_blocks (void *vdata)
{
    // This feature is disabled by default. Comment out this check to enable it..
    if (!seaf->check_enc_blocks) {
        return NULL;
    }

    // Wait for a while, and auto sync will set the repo's effective_host property.
    g_usleep (60  * G_USEC_PER_SEC);

    GList *ptr, *repos = seaf_repo_manager_get_repo_list(seaf->repo_mgr, -1, -1);
    if (!repos) {
        return NULL;
    }

    for (ptr = repos; ptr; ptr = ptr->next) {
        SeafRepo *repo = (SeafRepo*)ptr->data;
        char *value = load_repo_property (seaf->repo_mgr, repo->id, REPO_PROP_CHECK_BLOCKS);
        if (g_strcmp0 (value, "true") == 0) {
            g_free (value);
            continue;
        }
        g_free (value);

        if (!repo->encrypted || repo->enc_key[0] == '\0') {
            continue;
        }
        if (!repo->effective_host) {
            continue;
        }
        seaf_message ("Start to check enc repo blocks for repo %s.\n", repo->id);
        if (check_repo_corrupted_blocks(repo) < 0) {
            continue;
        }
        seaf_message ("Finish to check enc repo blocks for repo %s.\n", repo->id);
        save_repo_property (seaf->repo_mgr, repo->id, REPO_PROP_CHECK_BLOCKS, "true");
    }

    g_list_free (repos);
    return NULL;
}

int
seaf_repo_manager_start (SeafRepoManager *mgr)
{
    pthread_t tid;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    int rc;

    watch_repos (mgr);

    rc = pthread_create (&tid, &attr, cleanup_deleted_stores, NULL);
    if (rc != 0) {
        seaf_warning ("Failed to start cleanup thread: %s\n", strerror(rc));
    }

    rc = pthread_create (&tid, &attr, lock_office_file_worker,
                         mgr->priv->lock_office_job_queue);
    if (rc != 0) {
        seaf_warning ("Failed to start lock office file thread: %s\n", strerror(rc));
    }

    rc = pthread_create (&tid, &attr, check_corrupted_enc_blocks, NULL);
    if (rc != 0) {
        seaf_warning ("Failed to start check enc blocks thread: %s\n", strerror(rc));
    }

    return 0;
}

SeafRepo*
seaf_repo_manager_create_new_repo (SeafRepoManager *mgr,
                                   const char *name,
                                   const char *desc)
{
    SeafRepo *repo;
    char *repo_id;
    
    repo_id = gen_uuid ();
    repo = seaf_repo_new (repo_id, name, desc);
    if (!repo) {
        g_free (repo_id);
        return NULL;
    }
    g_free (repo_id);

    /* we directly create dir because it shouldn't exist */
    /* if (seaf_repo_mkdir (repo, base) < 0) { */
    /*     seaf_repo_free (repo); */
    /*     goto out; */
    /* } */

    seaf_repo_manager_add_repo (mgr, repo);
    return repo;
}

int
seaf_repo_manager_add_repo (SeafRepoManager *manager,
                            SeafRepo *repo)
{
    char sql[256];
    sqlite3 *db = manager->priv->db;

    pthread_mutex_lock (&manager->priv->db_lock);

    snprintf (sql, sizeof(sql), "REPLACE INTO Repo VALUES ('%s');", repo->id);
    sqlite_query_exec (db, sql);

    pthread_mutex_unlock (&manager->priv->db_lock);

    /* There may be a "deletion record" for this repo when it was deleted
     * last time.
     */
    seaf_repo_manager_remove_garbage_repo (manager, repo->id);

    repo->manager = manager;

    if (pthread_rwlock_wrlock (&manager->priv->lock) < 0) {
        seaf_warning ("[repo mgr] failed to lock repo cache.\n");
        return -1;
    }

    g_hash_table_insert (manager->priv->repo_hash, g_strdup(repo->id), repo);

    pthread_rwlock_unlock (&manager->priv->lock);

    return 0;
}

int
seaf_repo_manager_mark_repo_deleted (SeafRepoManager *mgr, SeafRepo *repo)
{
    char sql[256];

    pthread_mutex_lock (&mgr->priv->db_lock);

    snprintf (sql, sizeof(sql), "INSERT INTO DeletedRepo VALUES ('%s')",
              repo->id);
    if (sqlite_query_exec (mgr->priv->db, sql) < 0) {
        pthread_mutex_unlock (&mgr->priv->db_lock);
        return -1;
    }

    pthread_mutex_unlock (&mgr->priv->db_lock);

    repo->delete_pending = TRUE;

    return 0;
}

static gboolean
get_garbage_repo_id (sqlite3_stmt *stmt, void *vid_list)
{
    GList **ret = vid_list;
    char *repo_id;

    repo_id = g_strdup((const char *)sqlite3_column_text (stmt, 0));
    *ret = g_list_prepend (*ret, repo_id);

    return TRUE;
}

GList *
seaf_repo_manager_list_garbage_repos (SeafRepoManager *mgr)
{
    GList *repo_ids = NULL;

    pthread_mutex_lock (&mgr->priv->db_lock);

    sqlite_foreach_selected_row (mgr->priv->db,
                                 "SELECT repo_id FROM GarbageRepos",
                                 get_garbage_repo_id, &repo_ids);
    pthread_mutex_unlock (&mgr->priv->db_lock);

    return repo_ids;
}

void
seaf_repo_manager_remove_garbage_repo (SeafRepoManager *mgr, const char *repo_id)
{
    char sql[256];

    pthread_mutex_lock (&mgr->priv->db_lock);

    snprintf (sql, sizeof(sql), "DELETE FROM GarbageRepos WHERE repo_id='%s'",
              repo_id);
    sqlite_query_exec (mgr->priv->db, sql);

    pthread_mutex_unlock (&mgr->priv->db_lock);
}

void
seaf_repo_manager_remove_repo_ondisk (SeafRepoManager *mgr,
                                      const char *repo_id,
                                      gboolean add_deleted_record)
{
    char sql[256];

    /* We don't need to care about I/O errors here, since we can
     * GC any unreferenced repo data later.
     */

    if (add_deleted_record) {
        snprintf (sql, sizeof(sql), "REPLACE INTO GarbageRepos VALUES ('%s')",
                  repo_id);
        if (sqlite_query_exec (mgr->priv->db, sql) < 0)
            goto out;
    }

    /* Once the item in Repo table is deleted, the repo is gone.
     * This is the "commit point".
     */
    pthread_mutex_lock (&mgr->priv->db_lock);

    snprintf (sql, sizeof(sql), "DELETE FROM Repo WHERE repo_id = '%s'", repo_id);
    if (sqlite_query_exec (mgr->priv->db, sql) < 0)
        goto out;

    snprintf (sql, sizeof(sql), 
              "DELETE FROM DeletedRepo WHERE repo_id = '%s'", repo_id);
    sqlite_query_exec (mgr->priv->db, sql);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    /* remove index */
    char path[SEAF_PATH_MAX];
    snprintf (path, SEAF_PATH_MAX, "%s/%s", mgr->index_dir, repo_id);
    seaf_util_unlink (path);

    /* remove branch */
    GList *p;
    GList *branch_list = 
        seaf_branch_manager_get_branch_list (seaf->branch_mgr, repo_id);
    for (p = branch_list; p; p = p->next) {
        SeafBranch *b = (SeafBranch *)p->data;
        seaf_repo_manager_branch_repo_unmap (mgr, b);
        seaf_branch_manager_del_branch (seaf->branch_mgr, repo_id, b->name);
    }
    seaf_branch_list_free (branch_list);

    /* delete repo property firstly */
    seaf_repo_manager_del_repo_property (mgr, repo_id);

    pthread_mutex_lock (&mgr->priv->db_lock);

    snprintf (sql, sizeof(sql), "DELETE FROM RepoPasswd WHERE repo_id = '%s'", 
              repo_id);
    sqlite_query_exec (mgr->priv->db, sql);
    snprintf (sql, sizeof(sql), "DELETE FROM RepoKeys WHERE repo_id = '%s'", 
              repo_id);
    sqlite_query_exec (mgr->priv->db, sql);

    snprintf (sql, sizeof(sql), "DELETE FROM MergeInfo WHERE repo_id = '%s'", 
              repo_id);
    sqlite_query_exec (mgr->priv->db, sql);

    snprintf (sql, sizeof(sql), "DELETE FROM LockedFiles WHERE repo_id = '%s'",
              repo_id);
    sqlite_query_exec (mgr->priv->db, sql);

    snprintf (sql, sizeof(sql), "DELETE FROM FolderUserPerms WHERE repo_id = '%s'", 
              repo_id);
    sqlite_query_exec (mgr->priv->db, sql);

    snprintf (sql, sizeof(sql), "DELETE FROM FolderGroupPerms WHERE repo_id = '%s'", 
              repo_id);
    sqlite_query_exec (mgr->priv->db, sql);

    snprintf (sql, sizeof(sql), "DELETE FROM FolderPermTimestamp WHERE repo_id = '%s'", 
              repo_id);
    sqlite_query_exec (mgr->priv->db, sql);

    seaf_filelock_manager_remove (seaf->filelock_mgr, repo_id);

out:
    pthread_mutex_unlock (&mgr->priv->db_lock);
}

static char *
gen_deleted_store_path (const char *type, const char *repo_id)
{
    int n = 1;
    char *path = NULL;
    char *name = NULL;

    path = g_build_filename (seaf->deleted_store, type, repo_id, NULL);
    while (g_access(path, F_OK) == 0 && n < 10) {
        g_free (path);
        name = g_strdup_printf ("%s(%d)", repo_id, n);
        path = g_build_filename (seaf->deleted_store, type, name, NULL);
        g_free (name);
        ++n;
    }

    if (n == 10) {
        g_free (path);
        return NULL;
    }

    return path;
}

void
seaf_repo_manager_move_repo_store (SeafRepoManager *mgr,
                                   const char *type,
                                   const char *repo_id)
{
    char *src = NULL;
    char *dst = NULL;

    src = g_build_filename (seaf->seaf_dir, "storage", type, repo_id, NULL);
    dst = gen_deleted_store_path (type, repo_id);
    if (dst) {
        g_rename (src, dst);
    }
    g_free (src);
    g_free (dst);
}

/* Move commits, fs stores into "deleted_store" directory. */
static void
move_repo_stores (SeafRepoManager *mgr, SeafRepo *repo)
{
    seaf_repo_manager_move_repo_store (mgr, "commits", repo->id);
    seaf_repo_manager_move_repo_store (mgr, "fs", repo->id);
}

int
seaf_repo_manager_del_repo (SeafRepoManager *mgr,
                            SeafRepo *repo)
{
    seaf_repo_manager_remove_repo_ondisk (mgr, repo->id,
                                          (repo->version > 0) ? TRUE : FALSE);

    seaf_sync_manager_remove_active_path_info (seaf->sync_mgr, repo->id);

    remove_folder_perms (mgr, repo->id);

    move_repo_stores (mgr, repo);

    if (pthread_rwlock_wrlock (&mgr->priv->lock) < 0) {
        seaf_warning ("[repo mgr] failed to lock repo cache.\n");
        return -1;
    }

    g_hash_table_remove (mgr->priv->repo_hash, repo->id);

    pthread_rwlock_unlock (&mgr->priv->lock);

#if defined WIN32 || defined __APPLE__ || defined COMPILE_LINUX_WS
    seaf_notif_manager_unsubscribe_repo (seaf->notif_mgr, repo);
#endif

    seaf_repo_free (repo);

    return 0;
}

/*
  Return the internal Repo in hashtable. The caller should not free the returned Repo.
 */
SeafRepo*
seaf_repo_manager_get_repo (SeafRepoManager *manager, const gchar *id)
{
    SeafRepo *res;

    if (pthread_rwlock_rdlock (&manager->priv->lock) < 0) {
        seaf_warning ("[repo mgr] failed to lock repo cache.\n");
        return NULL;
    }

    res = g_hash_table_lookup (manager->priv->repo_hash, id);

    pthread_rwlock_unlock (&manager->priv->lock);

    if (res && !res->delete_pending)
        return res;

    return NULL;
}

gboolean
seaf_repo_manager_repo_exists (SeafRepoManager *manager, const gchar *id)
{
    SeafRepo *res;

    if (pthread_rwlock_rdlock (&manager->priv->lock) < 0) {
        seaf_warning ("[repo mgr] failed to lock repo cache.\n");
        return FALSE;
    }

    res = g_hash_table_lookup (manager->priv->repo_hash, id);

    pthread_rwlock_unlock (&manager->priv->lock);

    if (res && !res->delete_pending)
        return TRUE;
    
    return FALSE;
}

static int
save_branch_repo_map (SeafRepoManager *manager, SeafBranch *branch)
{
    char *sql;
    sqlite3 *db = manager->priv->db;

    pthread_mutex_lock (&manager->priv->db_lock);

    sql = sqlite3_mprintf ("REPLACE INTO RepoBranch VALUES (%Q, %Q)",
                           branch->repo_id, branch->name);
    sqlite_query_exec (db, sql);
    sqlite3_free (sql);

    pthread_mutex_unlock (&manager->priv->db_lock);

    return 0;
}

int
seaf_repo_manager_branch_repo_unmap (SeafRepoManager *manager, SeafBranch *branch)
{
    char *sql;
    sqlite3 *db = manager->priv->db;

    pthread_mutex_lock (&manager->priv->db_lock);

    sql = sqlite3_mprintf ("DELETE FROM RepoBranch WHERE branch_name = %Q"
                           " AND repo_id = %Q",
                           branch->name, branch->repo_id);
    if (sqlite_query_exec (db, sql) < 0) {
        seaf_warning ("Unmap branch repo failed\n");
        pthread_mutex_unlock (&manager->priv->db_lock);
        sqlite3_free (sql);
        return -1;
    }

    sqlite3_free (sql);
    pthread_mutex_unlock (&manager->priv->db_lock);

    return 0;
}

static void
load_repo_commit (SeafRepoManager *manager,
                  SeafRepo *repo,
                  SeafBranch *branch)
{
    SeafCommit *commit;

    commit = seaf_commit_manager_get_commit_compatible (manager->seaf->commit_mgr,
                                                        repo->id,
                                                        branch->commit_id);
    if (!commit) {
        seaf_warning ("Commit %s is missing\n", branch->commit_id);
        repo->is_corrupted = TRUE;
        return;
    }

    set_head_common (repo, branch);
    seaf_repo_from_commit (repo, commit);

    seaf_commit_unref (commit);
}

static gboolean
load_keys_cb (sqlite3_stmt *stmt, void *vrepo)
{
    SeafRepo *repo = vrepo;
    const char *key, *iv;

    key = (const char *)sqlite3_column_text(stmt, 0);
    iv = (const char *)sqlite3_column_text(stmt, 1);

    if (repo->enc_version == 1) {
        hex_to_rawdata (key, repo->enc_key, 16);
        hex_to_rawdata (iv, repo->enc_iv, 16);
    } else if (repo->enc_version >= 2) {
        hex_to_rawdata (key, repo->enc_key, 32);
        hex_to_rawdata (iv, repo->enc_iv, 16);
    }

    return FALSE;
}

static int
load_repo_passwd (SeafRepoManager *manager, SeafRepo *repo)
{
    sqlite3 *db = manager->priv->db;
    char sql[256];
    int n;

    pthread_mutex_lock (&manager->priv->db_lock);

    snprintf (sql, sizeof(sql), 
              "SELECT key, iv FROM RepoKeys WHERE repo_id='%s'",
              repo->id);
    n = sqlite_foreach_selected_row (db, sql, load_keys_cb, repo);
    if (n < 0) {
        pthread_mutex_unlock (&manager->priv->db_lock);
        return -1;
    }

    pthread_mutex_unlock (&manager->priv->db_lock);

    return 0;
    
}

static gboolean
load_property_cb (sqlite3_stmt *stmt, void *pvalue)
{
    char **value = pvalue;

    char *v = (char *) sqlite3_column_text (stmt, 0);
    *value = g_strdup (v);

    /* Only one result. */
    return FALSE;
}

static char *
load_repo_property (SeafRepoManager *manager,
                    const char *repo_id,
                    const char *key)
{
    sqlite3 *db = manager->priv->db;
    char sql[256];
    char *value = NULL;

    pthread_mutex_lock (&manager->priv->db_lock);

    snprintf(sql, 256, "SELECT value FROM RepoProperty WHERE "
             "repo_id='%s' and key='%s'", repo_id, key);
    if (sqlite_foreach_selected_row (db, sql, load_property_cb, &value) < 0) {
        seaf_warning ("Error read property %s for repo %s.\n", key, repo_id);
        pthread_mutex_unlock (&manager->priv->db_lock);
        return NULL;
    }

    pthread_mutex_unlock (&manager->priv->db_lock);

    return value;
}

static gboolean
load_branch_cb (sqlite3_stmt *stmt, void *vrepo)
{
    SeafRepo *repo = vrepo;
    SeafRepoManager *manager = repo->manager;

    char *branch_name = (char *) sqlite3_column_text (stmt, 0);
    SeafBranch *branch =
        seaf_branch_manager_get_branch (manager->seaf->branch_mgr,
                                        repo->id, branch_name);
    if (branch == NULL) {
        seaf_warning ("Broken branch name for repo %s\n", repo->id); 
        repo->is_corrupted = TRUE;
        return FALSE;
    }
    load_repo_commit (manager, repo, branch);
    seaf_branch_unref (branch);

    /* Only one result. */
    return FALSE;
}

static gboolean
is_wt_repo_name_same (const char *worktree, const char *repo_name)
{
    char *basename = g_path_get_basename (worktree);
    gboolean ret = FALSE;
    ret = (g_strcmp0 (basename, repo_name) == 0);
    g_free (basename);
    return ret;
}

static SeafRepo *
load_repo (SeafRepoManager *manager, const char *repo_id)
{
    char sql[256];

    SeafRepo *repo = seaf_repo_new(repo_id, NULL, NULL);
    if (!repo) {
        seaf_warning ("[repo mgr] failed to alloc repo.\n");
        return NULL;
    }

    repo->manager = manager;

    snprintf(sql, 256, "SELECT branch_name FROM RepoBranch WHERE repo_id='%s'",
             repo->id);
    if (sqlite_foreach_selected_row (manager->priv->db, sql, 
                                     load_branch_cb, repo) < 0) {
        seaf_warning ("Error read branch for repo %s.\n", repo->id);
        seaf_repo_free (repo);
        return NULL;
    }

    /* If repo head is set but failed to load branch or commit. */
    if (repo->is_corrupted) {
        seaf_repo_free (repo);
        /* remove_repo_ondisk (manager, repo_id); */
        return NULL;
    }

    /* Repo head may be not set if it's just cloned but not checked out yet. */
    if (repo->head == NULL) {
        /* the repo do not have a head branch, try to load 'master' branch */
        SeafBranch *branch =
            seaf_branch_manager_get_branch (manager->seaf->branch_mgr,
                                            repo->id, "master");
        if (branch != NULL) {
             SeafCommit *commit;

             commit =
                 seaf_commit_manager_get_commit_compatible (manager->seaf->commit_mgr,
                                                            repo->id,
                                                            branch->commit_id);
             if (commit) {
                 seaf_repo_from_commit (repo, commit);
                 seaf_commit_unref (commit);
             } else {
                 seaf_warning ("[repo-mgr] Can not find commit %s\n",
                            branch->commit_id);
                 repo->is_corrupted = TRUE;
             }

             seaf_branch_unref (branch);
        } else {
            seaf_warning ("[repo-mgr] Failed to get branch master");
            repo->is_corrupted = TRUE;
        }
    }

    if (repo->is_corrupted) {
        seaf_repo_free (repo);
        /* remove_repo_ondisk (manager, repo_id); */
        return NULL;
    }

    load_repo_passwd (manager, repo);

    char *value;

    value = load_repo_property (manager, repo->id, REPO_AUTO_SYNC);
    if (g_strcmp0(value, "false") == 0) {
        repo->auto_sync = 0;
    }
    g_free (value);

    repo->worktree = load_repo_property (manager, repo->id, "worktree");
    if (repo->worktree)
        repo->worktree_invalid = FALSE;

    repo->email = load_repo_property (manager, repo->id, REPO_PROP_EMAIL);
    repo->username = load_repo_property (manager, repo->id, REPO_PROP_USERNAME);
    repo->token = load_repo_property (manager, repo->id, REPO_PROP_TOKEN);

    /* May be NULL if this property is not set in db. */
    repo->server_url = load_repo_property (manager, repo->id, REPO_PROP_SERVER_URL);

    if (repo->head != NULL && seaf_repo_check_worktree (repo) < 0) {
        if (seafile_session_config_get_allow_invalid_worktree(seaf)) {
            seaf_warning ("Worktree for repo \"%s\" is invalid, but still keep it.\n",
                          repo->name);
            repo->worktree_invalid = TRUE;
        } else {
            seaf_message ("Worktree for repo \"%s\" is invalid, delete it.\n",
                          repo->name);
            seaf_repo_manager_del_repo (manager, repo);
            return NULL;
        }
    }

    /* load readonly property */
    value = load_repo_property (manager, repo->id, REPO_PROP_IS_READONLY);
    if (g_strcmp0(value, "true") == 0)
        repo->is_readonly = TRUE;
    else
        repo->is_readonly = FALSE;
    g_free (value);

    /* load sync period property */
    value = load_repo_property (manager, repo->id, REPO_PROP_SYNC_INTERVAL);
    if (value) {
        int interval = atoi(value);
        if (interval > 0)
            repo->sync_interval = interval;
    }
    g_free (value);

    if (repo->worktree) {
        gboolean wt_repo_name_same = is_wt_repo_name_same (repo->worktree, repo->name);
        value = load_repo_property (manager, repo->id, REPO_SYNC_WORKTREE_NAME);
        if (g_strcmp0 (value, "true") == 0) {
            /* If need to sync worktree name with library name, update worktree folder name. */
            if (!wt_repo_name_same)
                update_repo_worktree_name (repo, repo->name, FALSE);
        } else {
            /* If an existing repo's worktree folder name is the same as repo name, but
             * sync_worktree_name property is not set, set it.
             */
            if (wt_repo_name_same)
                save_repo_property (manager, repo->id, REPO_SYNC_WORKTREE_NAME, "true");
        }
        g_free (value);
    }

    /* load empty enc key property */
    value = load_repo_property (manager, repo->id, REPO_PROP_EMPTY_ENC_KEY);
    if (g_strcmp0(value, "true") == 0)
        repo->empty_enc_key = TRUE;
    else
        repo->empty_enc_key = FALSE;
    g_free (value);

    g_hash_table_insert (manager->priv->repo_hash, g_strdup(repo->id), repo);

    return repo;
}

static sqlite3*
open_db (SeafRepoManager *manager, const char *seaf_dir)
{
    sqlite3 *db;
    char *db_path;

    db_path = g_build_filename (seaf_dir, "repo.db", NULL);
    if (sqlite_open_db (db_path, &db) < 0)
        return NULL;
    g_free (db_path);
    manager->priv->db = db;

    char *sql = "CREATE TABLE IF NOT EXISTS Repo (repo_id TEXT PRIMARY KEY);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS DeletedRepo (repo_id TEXT PRIMARY KEY);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS RepoBranch ("
        "repo_id TEXT PRIMARY KEY, branch_name TEXT);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS RepoLanToken ("
        "repo_id TEXT PRIMARY KEY, token TEXT);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS RepoTmpToken ("
        "repo_id TEXT, peer_id TEXT, token TEXT, timestamp INTEGER, "
        "PRIMARY KEY (repo_id, peer_id));";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS RepoPasswd "
        "(repo_id TEXT PRIMARY KEY, passwd TEXT NOT NULL);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS RepoKeys "
        "(repo_id TEXT PRIMARY KEY, key TEXT NOT NULL, iv TEXT NOT NULL);";
    sqlite_query_exec (db, sql);
    
    sql = "CREATE TABLE IF NOT EXISTS RepoProperty ("
        "repo_id TEXT, key TEXT, value TEXT);";
    sqlite_query_exec (db, sql);

    sql = "CREATE INDEX IF NOT EXISTS RepoIndex ON RepoProperty (repo_id);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS MergeInfo ("
        "repo_id TEXT PRIMARY KEY, in_merge INTEGER, branch TEXT);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS CommonAncestor ("
        "repo_id TEXT PRIMARY KEY, ca_id TEXT, head_id TEXT);";
    sqlite_query_exec (db, sql);

    /* Version 1 repos will be added to this table after deletion.
     * GC will scan this table and remove the objects and blocks for the repos.
     */
    sql = "CREATE TABLE IF NOT EXISTS GarbageRepos (repo_id TEXT PRIMARY KEY);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS LockedFiles (repo_id TEXT, path TEXT, "
        "operation TEXT, old_mtime INTEGER, file_id TEXT, new_path TEXT, "
        "PRIMARY KEY (repo_id, path));";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS FolderUserPerms ("
        "repo_id TEXT, path TEXT, permission TEXT);";
    sqlite_query_exec (db, sql);

    sql = "CREATE INDEX IF NOT EXISTS folder_user_perms_repo_id_idx "
        "ON FolderUserPerms (repo_id);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS FolderGroupPerms ("
        "repo_id TEXT, path TEXT, permission TEXT);";
    sqlite_query_exec (db, sql);

    sql = "CREATE INDEX IF NOT EXISTS folder_group_perms_repo_id_idx "
        "ON FolderGroupPerms (repo_id);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS FolderPermTimestamp ("
        "repo_id TEXT, timestamp INTEGER, PRIMARY KEY (repo_id));";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS ServerProperty ("
        "server_url TEXT, key TEXT, value TEXT, PRIMARY KEY (server_url, key));";
    sqlite_query_exec (db, sql);

    sql = "CREATE INDEX IF NOT EXISTS ServerIndex ON ServerProperty (server_url);";
    sqlite_query_exec (db, sql);

    sql = "CREATE TABLE IF NOT EXISTS FileSyncError ("
        "id INTEGER PRIMARY KEY AUTOINCREMENT, repo_id TEXT, repo_name TEXT, "
        "path TEXT, err_id INTEGER, timestamp INTEGER);";
    sqlite_query_exec (db, sql);

    sql = "CREATE INDEX IF NOT EXISTS FileSyncErrorIndex ON FileSyncError (repo_id, path)";
    sqlite_query_exec (db, sql);

    return db;
}

static gboolean
load_repo_cb (sqlite3_stmt *stmt, void *vmanager)
{
    SeafRepoManager *manager = vmanager;
    const char *repo_id;

    repo_id = (const char *) sqlite3_column_text (stmt, 0);

    load_repo (manager, repo_id);

    return TRUE;
}

static gboolean
remove_deleted_repo (sqlite3_stmt *stmt, void *vmanager)
{
    SeafRepoManager *manager = vmanager;
    const char *repo_id;

    repo_id = (const char *) sqlite3_column_text (stmt, 0);

    seaf_repo_manager_remove_repo_ondisk (manager, repo_id, TRUE);

    return TRUE;
}

static void
load_repos (SeafRepoManager *manager, const char *seaf_dir)
{
    sqlite3 *db = open_db(manager, seaf_dir);
    if (!db) return;

    char *sql;

    sql = "SELECT repo_id FROM DeletedRepo";
    if (sqlite_foreach_selected_row (db, sql, remove_deleted_repo, manager) < 0) {
        seaf_warning ("Error removing deleted repos.\n");
        return;
    }

    sql = "SELECT repo_id FROM Repo;";
    if (sqlite_foreach_selected_row (db, sql, load_repo_cb, manager) < 0) {
        seaf_warning ("Error read repo db.\n");
        return;
    }
}

static void
save_repo_property (SeafRepoManager *manager,
                    const char *repo_id,
                    const char *key, const char *value)
{
    char *sql;
    sqlite3 *db = manager->priv->db;

    pthread_mutex_lock (&manager->priv->db_lock);

    sql = sqlite3_mprintf ("SELECT repo_id FROM RepoProperty WHERE repo_id=%Q AND key=%Q",
                           repo_id, key);
    if (sqlite_check_for_existence(db, sql)) {
        sqlite3_free (sql);
        sql = sqlite3_mprintf ("UPDATE RepoProperty SET value=%Q"
                               "WHERE repo_id=%Q and key=%Q",
                               value, repo_id, key);
        sqlite_query_exec (db, sql);
        sqlite3_free (sql);
    } else {
        sqlite3_free (sql);
        sql = sqlite3_mprintf ("INSERT INTO RepoProperty VALUES (%Q, %Q, %Q)",
                               repo_id, key, value);
        sqlite_query_exec (db, sql);
        sqlite3_free (sql);
    }

    pthread_mutex_unlock (&manager->priv->db_lock);
}

int
seaf_repo_manager_set_repo_property (SeafRepoManager *manager, 
                                     const char *repo_id,
                                     const char *key,
                                     const char *value)
{
    SeafRepo *repo;

    repo = seaf_repo_manager_get_repo (manager, repo_id);
    if (!repo)
        return -1;

    if (strcmp(key, REPO_AUTO_SYNC) == 0) {
        if (!seaf->started) {
            seaf_message ("System not started, skip setting auto sync value.\n");
            return 0;
        }

        if (g_strcmp0(value, "true") == 0) {
            repo->auto_sync = 1;
            if (repo->sync_interval == 0)
                seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id,
                                            repo->worktree);
            repo->last_sync_time = 0;
        } else {
            repo->auto_sync = 0;
            if (repo->sync_interval == 0)
                seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id);
            /* Cancel current sync task if any. */
            seaf_sync_manager_cancel_sync_task (seaf->sync_mgr, repo->id);
            seaf_sync_manager_remove_active_path_info (seaf->sync_mgr, repo->id);
        }
    }

    if (strcmp(key, REPO_PROP_SYNC_INTERVAL) == 0) {
        if (!seaf->started) {
            seaf_message ("System not started, skip setting auto sync value.\n");
            return 0;
        }

        int interval = atoi(value);

        if (interval > 0) {
            repo->sync_interval = interval;
            if (repo->auto_sync)
                seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id);
        } else {
            repo->sync_interval = 0;
            if (repo->auto_sync)
                seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id,
                                            repo->worktree);
        }
    }

    if (strcmp (key, REPO_PROP_SERVER_URL) == 0) {
        char *url = canonical_server_url (value);

        if (!repo->server_url) {
            /* Called from clone-mgr. */
            repo->server_url = url;
        } else {
            g_free (repo->server_url);
            repo->server_url = url;

            g_free (repo->effective_host);
            repo->effective_host = NULL;
        }

        save_repo_property (manager, repo_id, key, url);
        return 0;
    }

    if (strcmp (key, REPO_PROP_IS_READONLY) == 0) {
       if (g_strcmp0 (value, "true") == 0)
           repo->is_readonly = TRUE;
       else
           repo->is_readonly = FALSE;
    }

    if (strcmp (key, REPO_PROP_USERNAME) == 0) {
        if (!repo->username) {
            /* Called from clone-mgr. */
            repo->username = g_strdup (value);
        } else {
            g_free (repo->username);
            repo->username = g_strdup(value);
        }

        save_repo_property (manager, repo_id, key, value);
        return 0;
    }

    save_repo_property (manager, repo_id, key, value);
    return 0;
}

char *
seaf_repo_manager_get_repo_property (SeafRepoManager *manager, 
                                     const char *repo_id,
                                     const char *key)
{
    return load_repo_property (manager, repo_id, key);
}

static void
seaf_repo_manager_del_repo_property (SeafRepoManager *manager, 
                                     const char *repo_id)
{
    char *sql;
    sqlite3 *db = manager->priv->db;

    pthread_mutex_lock (&manager->priv->db_lock);

    sql = sqlite3_mprintf ("DELETE FROM RepoProperty WHERE repo_id = %Q", repo_id);
    sqlite_query_exec (db, sql);
    sqlite3_free (sql);

    pthread_mutex_unlock (&manager->priv->db_lock);
}

static void
seaf_repo_manager_del_repo_property_by_key (SeafRepoManager *manager,
                                            const char *repo_id,
                                            const char *key)
{
    char *sql;
    sqlite3 *db = manager->priv->db;

    pthread_mutex_lock (&manager->priv->db_lock);

    sql = sqlite3_mprintf ("DELETE FROM RepoProperty "
                           "WHERE repo_id = %Q "
                           "  AND key = %Q", repo_id, key);
    sqlite_query_exec (db, sql);
    sqlite3_free (sql);

    pthread_mutex_unlock (&manager->priv->db_lock);
}

static int
save_repo_enc_info (SeafRepoManager *manager,
                    SeafRepo *repo)
{
    sqlite3 *db = manager->priv->db;
    char sql[512];
    char key[65], iv[33];

    if (repo->enc_version == 1) {
        rawdata_to_hex (repo->enc_key, key, 16);
        rawdata_to_hex (repo->enc_iv, iv, 16);
    } else if (repo->enc_version >= 2) {
        rawdata_to_hex (repo->enc_key, key, 32);
        rawdata_to_hex (repo->enc_iv, iv, 16);
    }

    snprintf (sql, sizeof(sql), "REPLACE INTO RepoKeys VALUES ('%s', '%s', '%s')",
              repo->id, key, iv);
    if (sqlite_query_exec (db, sql) < 0)
        return -1;

    return 0;
}

int 
seaf_repo_manager_set_repo_passwd (SeafRepoManager *manager,
                                   SeafRepo *repo,
                                   const char *passwd)
{
    int ret;

    if (seafile_decrypt_repo_enc_key (repo->enc_version, passwd, repo->random_key,
                                      repo->salt,
                                      repo->enc_key, repo->enc_iv) < 0)
        return -1;

    pthread_mutex_lock (&manager->priv->db_lock);

    ret = save_repo_enc_info (manager, repo);

    pthread_mutex_unlock (&manager->priv->db_lock);

    return ret;
}

int 
seaf_repo_manager_save_repo_enc_info (SeafRepoManager *manager,
                                      const char *repo_id,
                                      const char *key,
                                      const char *iv)
{
    sqlite3 *db = manager->priv->db;
    char sql[512];

    snprintf (sql, sizeof(sql), "REPLACE INTO RepoKeys VALUES ('%s', '%s', '%s')",
              repo_id, key, iv);
    if (sqlite_query_exec (db, sql) < 0)
        return -1;

    return 0;
}

int 
seaf_repo_manager_load_repo_enc_info (SeafRepoManager *manager,
                                      SeafRepo *repo)
{

    return load_repo_passwd (manager, repo);
}

GList*
seaf_repo_manager_get_repo_list (SeafRepoManager *manager, int start, int limit)
{
    GList *repo_list = NULL;
    GHashTableIter iter;
    SeafRepo *repo;
    gpointer key, value;

    if (pthread_rwlock_rdlock (&manager->priv->lock) < 0) {
        seaf_warning ("[repo mgr] failed to lock repo cache.\n");
        return NULL;
    }
    g_hash_table_iter_init (&iter, manager->priv->repo_hash);

    while (g_hash_table_iter_next (&iter, &key, &value)) {
        repo = value;
        if (!repo->delete_pending)
            repo_list = g_list_prepend (repo_list, repo);
    }

    pthread_rwlock_unlock (&manager->priv->lock);

    return repo_list;
}

GList *
seaf_repo_manager_get_repo_id_list_by_server (SeafRepoManager *manager, const char *server_url)
{
    GList *repo_id_list = NULL;
    GHashTableIter iter;
    SeafRepo *repo;
    gpointer key, value;

    if (pthread_rwlock_rdlock (&manager->priv->lock) < 0) {
        seaf_warning ("[repo mgr] failed to lock repo cache.\n");
        return NULL;
    }
    g_hash_table_iter_init (&iter, manager->priv->repo_hash);

    while (g_hash_table_iter_next (&iter, &key, &value)) {
        repo = value;
        if (!repo->delete_pending && g_strcmp0 (repo->server_url, server_url) == 0)
            repo_id_list = g_list_prepend (repo_id_list, g_strdup(repo->id));
    }

    pthread_rwlock_unlock (&manager->priv->lock);

    return repo_id_list;
}

int
seaf_repo_manager_set_repo_email (SeafRepoManager *mgr,
                                  SeafRepo *repo,
                                  const char *email)
{
    g_free (repo->email);
    repo->email = g_strdup(email);

    save_repo_property (mgr, repo->id, REPO_PROP_EMAIL, email);
    return 0;
}

int
seaf_repo_manager_set_repo_token (SeafRepoManager *manager, 
                                  SeafRepo *repo,
                                  const char *token)
{
    g_free (repo->token);
    repo->token = g_strdup(token);

    save_repo_property (manager, repo->id, REPO_PROP_TOKEN, token);
    return 0;
}


int
seaf_repo_manager_remove_repo_token (SeafRepoManager *manager,
                                     SeafRepo *repo)
{
    g_free (repo->token);
    repo->token = NULL;
    seaf_repo_manager_del_repo_property_by_key(manager, repo->id, REPO_PROP_TOKEN);
    return 0;
}

int
seaf_repo_manager_set_repo_relay_info (SeafRepoManager *mgr,
                                       const char *repo_id,
                                       const char *relay_addr,
                                       const char *relay_port)
{
    save_repo_property (mgr, repo_id, REPO_PROP_RELAY_ADDR, relay_addr);
    save_repo_property (mgr, repo_id, REPO_PROP_RELAY_PORT, relay_port);
    return 0;
}

void
seaf_repo_manager_get_repo_relay_info (SeafRepoManager *mgr,
                                       const char *repo_id,
                                       char **relay_addr,
                                       char **relay_port)
{
    char *addr, *port;

    addr = load_repo_property (mgr, repo_id, REPO_PROP_RELAY_ADDR);
    port = load_repo_property (mgr, repo_id, REPO_PROP_RELAY_PORT);

    if (relay_addr && addr)
        *relay_addr = addr;
    if (relay_port && port)
        *relay_port = port;
}

static void
update_server_properties (SeafRepoManager *mgr,
                          const char *repo_id,
                          const char *new_server_url)
{
    char *old_server_url = NULL;
    char *sql = NULL;

    old_server_url = seaf_repo_manager_get_repo_property (mgr, repo_id,
                                                          REPO_PROP_SERVER_URL);
    if (!old_server_url)
        return;

    pthread_mutex_lock (&mgr->priv->db_lock);

    sql = sqlite3_mprintf ("UPDATE ServerProperty SET server_url=%Q WHERE "
                           "server_url=%Q;", new_server_url, old_server_url);
    sqlite_query_exec (mgr->priv->db, sql);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    sqlite3_free (sql);
    g_free (old_server_url);
}

int
seaf_repo_manager_update_repos_server_host (SeafRepoManager *mgr,
                                            const char *old_server_url,
                                            const char *new_server_url)
{
    GList *ptr, *repos = seaf_repo_manager_get_repo_list (seaf->repo_mgr, 0, -1);
    SeafRepo *r;
    char *canon_old_server_url = canonical_server_url(old_server_url);    
    char *canon_new_server_url = canonical_server_url(new_server_url);

    for (ptr = repos; ptr; ptr = ptr->next) {
        r = ptr->data;
        
        char *server_url = seaf_repo_manager_get_repo_property (seaf->repo_mgr,
                                                                r->id,
                                                                REPO_PROP_SERVER_URL);
        
        if (g_strcmp0(server_url, canon_old_server_url) == 0) {
            /* Update server property before server_url is changed. */
            update_server_properties (mgr, r->id, canon_new_server_url);

            seaf_repo_manager_set_repo_property (
                seaf->repo_mgr, r->id, REPO_PROP_SERVER_URL, canon_new_server_url);
        }
        g_free (server_url);

    }

    g_list_free (repos);
    g_free (canon_old_server_url);
    g_free (canon_new_server_url);

    return 0;
}

char *
seaf_repo_manager_get_server_property (SeafRepoManager *mgr,
                                       const char *server_url,
                                       const char *key)
{
    char *sql = sqlite3_mprintf ("SELECT value FROM ServerProperty WHERE "
                                 "server_url=%Q AND key=%Q;",
                                 server_url, key);
    char *value;

    pthread_mutex_lock (&mgr->priv->db_lock);

    value = sqlite_get_string (mgr->priv->db, sql);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    sqlite3_free (sql);
    return value;
}

int
seaf_repo_manager_set_server_property (SeafRepoManager *mgr,
                                       const char *server_url,
                                       const char *key,
                                       const char *value)
{
    char *sql;
    int ret;
    char *canon_server_url = canonical_server_url(server_url);

    pthread_mutex_lock (&mgr->priv->db_lock);

    sql = sqlite3_mprintf ("REPLACE INTO ServerProperty VALUES (%Q, %Q, %Q);",
                           canon_server_url, key, value);
    ret = sqlite_query_exec (mgr->priv->db, sql);

    pthread_mutex_unlock (&mgr->priv->db_lock);

    sqlite3_free (sql);
    g_free (canon_server_url);
    return ret;
}

gboolean
seaf_repo_manager_server_is_pro (SeafRepoManager *mgr,
                                 const char *server_url)
{
    gboolean ret = FALSE;

    char *is_pro = seaf_repo_manager_get_server_property (seaf->repo_mgr,
                                                          server_url,
                                                          SERVER_PROP_IS_PRO);
    if (is_pro != NULL && strcasecmp (is_pro, "true") == 0)
        ret = TRUE;

    g_free (is_pro);
    return ret;
}

/*
 * Read ignored files from ignore.txt
 */
GList *seaf_repo_load_ignore_files (const char *worktree)
{
    GList *list = NULL;
    SeafStat st;
    FILE *fp;
    char *full_path, *pattern;
    char path[SEAF_PATH_MAX];

    full_path = g_build_path (PATH_SEPERATOR, worktree,
                              IGNORE_FILE, NULL);
    if (seaf_stat (full_path, &st) < 0)
        goto error;
    if (!S_ISREG(st.st_mode))
        goto error;
    fp = g_fopen(full_path, "r");
    if (fp == NULL)
        goto error;

    while (fgets(path, SEAF_PATH_MAX, fp) != NULL) {
        /* remove leading and trailing whitespace, including \n \r. */
        g_strstrip (path);

        /* ignore comment and blank line */
        if (path[0] == '#' || path[0] == '\0')
            continue;

        /* Change 'foo/' to 'foo/ *'. */
        if (path[strlen(path)-1] == '/')
            pattern = g_strdup_printf("%s/%s*", worktree, path);
        else
            pattern = g_strdup_printf("%s/%s", worktree, path);

        list = g_list_prepend(list, pattern);
    }

    fclose(fp);
    g_free (full_path);
    return list;

error:
    g_free (full_path);
    return NULL;
}

gboolean
seaf_repo_check_ignore_file (GList *ignore_list, const char *fullpath)
{
    char *str;
    SeafStat st;
    GPatternSpec *ignore_spec;
    GList *p;

    str = g_strdup(fullpath);

    int rc = seaf_stat(str, &st);
    if (rc == 0 && S_ISDIR(st.st_mode)) {
        g_free (str);
        str = g_strconcat (fullpath, "/", NULL);
    }

    for (p = ignore_list; p != NULL; p = p->next) {
        char *pattern = (char *)p->data;

        ignore_spec = g_pattern_spec_new(pattern);
        if (g_pattern_match_string(ignore_spec, str)) {
            g_free (str);
            g_pattern_spec_free(ignore_spec);
            return TRUE;
        }
        g_pattern_spec_free(ignore_spec);
    }

    g_free (str);
    return FALSE;
}

/*
 * Free ignored file list
 */
void seaf_repo_free_ignore_files (GList *ignore_list)
{
    GList *p;

    if (ignore_list == NULL)
        return;

    for (p = ignore_list; p != NULL; p = p->next)
        free(p->data);

    g_list_free (ignore_list);
}
